Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 2

Este post é parte de uma série em que construo incrementalmente a ferramenta de injeção de dependência Reader, usando diferentes conceitos avançados de programação funcional em scala:

  1. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 1
  2. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 2
  3. Caril e Bolo sem indigestão de tipo – Covariância, Contravariância e a Mônada do Leitor
  4. Ferramentas do Reader Monad
  5. Generalizando o Reader Tooling, parte 1
  6. Generalizando o Reader Tooling, parte 2

Esta é uma continuação de um post anterior sobre injeção de dependência para configurar a conexão de banco de dados Play 2 , eu recomendo que você dê uma olhada primeiro para ter um pouco mais de contexto.

Coma um pouco de Bolo

No post anterior, eu discuti uma maneira de passar uma configuração para jogar ‘s DB.withConnection, sem envenenar seus argumentos de função listas com detalhes de implementação. Isso foi conseguido usando implícitos, passando um Stringcom a chave de configuração a ser usada para o banco de dados SQL

No entanto, se você já trabalhou em um grande aplicativo da web, saberá que um String para configurar uma conexão não é realmente suficiente. O exemplo dado na documentação do Play é que você pode ter vários bancos de dados SQL como armazenamento de dados, mas cada vez mais os aplicativos da web dependem de outros tipos de armazenamento, apoiados por bancos de dados NoSQL ou acessados ​​por meio de serviços da web.

Antes de entrar nos detalhes de outro método de injeção de dependência, vamos refatorar um pouco o código para poder trocar mais facilmente o tipo de armazenamento de dados usado em nosso aplicativo.

No post anterior, tivemos um exemplo de uma classe de modelo para um usuário, com um objeto complementar contendo as funções de acesso a dados:

case class User(name: String, email: String)

object User {
def findAll: Seq[User] = DB.withConnection { implicit conn =>
SQL
(…))…
}
}

Agora, se você deseja trocar seu armazenamento SQL por NoSQL ou um serviço da web, seu Userobjeto se torna obsoleto, você terá que alterar todo o seu código para usar um novo objeto Usuário. Se a sua alteração for definitiva, não é tão ruim, mas imagine que você deseja avaliar soluções diferentes ou, mais comumente, imagine que em seus testes, você deseja ter um armazenamento de dados “falso” que não dependa das consultas SQL. Você precisará de algo um pouco mais modular.

Uma solução é depender de uma interface de conexão separada, que define o acesso aos dados. Por conveniência, se desejar, você ainda pode ter um atalho em seu objeto companheiro Usuário. Portanto, primeiro você define uma característica que descreve sua conexão. Você pode criar esse objeto de conexão para os diferentes tipos de acesso que possa ter. Por exemplo, uma característica que descreve a fonte de dados para os usuários, outra para as conexões de seus amigos (que pode ser implementada como uma chamada para a API do Facebook, por exemplo), etc .:

trait UserConnection {
def findAllUsers: Seq[User]
def createUser(u: User): Either[Throwable, User]
}

trait
SocialNetworkConnection {
def getFriends(u: User): Seq[User]
}

Agora, sua função de atalho no objeto complementar User vai depender desse traço de conexão, se seguirmos o padrão da última postagem, usando implícitos, você terá algo assim:

object User {
def findAll(implicit conn: UserConnection): Seq[User] = conn.findAll
def getFriends(u: User)(implicit conn: SocialNetworkConnection): Seq[User] = conn.getFriends(u)
}

Você pode então passar essa dependência, com implícita, nas funções de chamada, imagine:

def avgNumberFriends(implicit conn: UserConnection with SocialNetworkConnection): Double = {
val allUsers
= User.findAll.view
allUsers
.map(User.getFriends(_).length).reduce(_ + _).toDouble / allUsers.length
}

Isso nos dá uma boa abstração e podemos fazer diferentes características para diferentes tipos de objeto de modelo. Podemos então usar o padrão bolo para construir uma implementação válida, por exemplo:

class MySQLConnection(dbName: String) extends UserConnection with SocialNetworkConnection 

Que você pode instanciar da maneira certa, dependendo do seu contexto e requisitos de teste.implicit val

Comer um pouco de curry

Ok, então fizemos uma pequena refatoração para abstrair o tipo de conexão, mas ainda temos esses implícitos feios e confusos. Como mencionei no post anterior, eu os acho um pouco irritantes, pois escondem muito do que está acontecendo e se você não tiver cuidado, rapidamente terá confrontos no contexto implícito.

Em sua apresentação Dead-Simple Dependency Injection , Runar Oli apresentou uma abordagem interessante para fazer injeção de dependência sem usar implícitos. Se você prefere apresentações de vídeo a leitura, vá direto para a apresentação dele, é muito bom e cobre tudo o que vou discutir aqui. Nos parágrafos a seguir, vou apenas resumir a ideia e mostrar como ela pode ser usada no contexto de um aplicativo Play 2 . Embora eu use o Play 2 como exemplo, este é realmente um padrão que pode ser usado em qualquer código scala em que você dependa de recursos que podem ser trocados, portanto, continue lendo mesmo se não usar o Play.

A ideia é que, em vez de usar implícitos, você use currying para declarar a dependência e passá-la para cima em sua cadeia de chamadas. Simplificando, currying é um conceito de programação funcional em que sua função, em vez de retornar um resultado final, retorna uma nova função. Observe que a notação implícita em scala é um açúcar de sintaxe para currying, com pesquisa adicional no contexto para os implícitos.

Portanto, agora, em vez de usar um implicitargumento, você retorna uma função:

object User {
def findAll: UserConnection => Seq[User] = conn => conn.findAll
def getFriends(u: User): SocialNetworkConnection => Seq[User] = conn => conn.getFriends(u)
}

Essa notação significa que você está retornando uma nova função tomando como argumento a UserConnectione eventualmente retornando a . No momento, não é muito útil em comparação com os implícitos, imagine nossa chamada anterior combinada para as duas conexões:Seq[User]

def avgNumberFriends: UserConnection with SocialNetworkConnection => Double = conn => {
val allUsers
= User.findAll(conn).view
allUsers
.map(User.getFriends(_)(conn).length).reduce(_ + _).toDouble / allUsers.length
}

Você precisa adicionar um parâmetro extra para obter o resultado real e poder trabalhar nele. Vejo você já pensando FTW!(conn)implicits

Mas espere! Existe um padrão de programação funcional que existe para nos ajudar … Mônadas!

The Reader Monad

Se você viu a palavra mônada e está com medo, não tenha, eu realmente não vou me aprofundar muito aqui, usaremos uma mônada, mas você não precisa saber todos os detalhes sangrentos para poder para usá-lo.

O Scalaz inclui a mônada do Reader, mas aqui está o código se você não quiser incluir tudo apenas para fazer este truque de DI:

object ReaderMonad {

implicit def reader[From, To](f: From => To) = Reader(f)

case class Reader[From, To](wrappedF: From => To) {

def apply(c: From) = wrappedF(c)

def map[Tob](transformF: To => Tob): Reader[From, Tob] =
Reader(c => transformF(wrappedF(c)))

def flatMap[Tob](transformF: To => Reader[From, Tob]): Reader[From, Tob] =
Reader(c => transformF(wrappedF(c))(c))

}

def pure[From, To](a: To) = Reader((c: From) => a)

}

Vamos dar uma olhada nisso, não tenha medo;)

Definimos uma classe Readerque possui uma propriedade:, wrappedFque é uma função. Esta função é o que nós queremos esconder em nosso exemplo anterior: . O Reader realmente envolve nosso tipo de retorno de função e nos permite ter uma assinatura mais limpa, mas também fazer algumas manipulações interessantes em nosso tipo de retorno de função.UserConnection => Seq[User]

A função na classe Reader é apenas um atalho para aplicar a função que ela envolve, como você pode ver, a única coisa que faz é chamar com o parâmetro fornecido.apply(c: From)wrappedF

O Readerleva dois parâmetros de tipo para descrever a função que envolve:

  • From o tipo de argumento da função
  • To o tipo de retorno da função

O mape flatMapnos permitem manipular o tipo de retorno da função agrupada. Isso é muito semelhante às funções mape flatMapnas coleções em scala. Por exemplo, uma sequência de Int:

val intSeq: Seq[Int] = Seq(1, 2, 3, 4)

pode ser transformado em uma sequência de String:

val strSeq: Seq[String] = intSeq.map(_.toString)

Em nosso exemplo anterior, temos:, nosso problema, é que retorna um tipo de função, de para , mas queremos apenas um comprimento, então gostaríamos de “mapear” o para um . Se usarmos um Reader, podemos fazer exatamente isso:User.getFriends(_)(conn).lengthUser.getFriendsSocialNetworkConnectionSeq[User]Seq[User]Int

object User {
def findAll: Reader[UserConnection,Seq[User]] = conn => conn.findAll
def getFriends(u: User): Reader[SocialNetworkConnection,Seq[User]] = conn => conn.getFriends(u)
}

def numberOfFriends(u: User): Reader[SocialNetworkConnection,Int] = User.getFriends(u).map(_.length)

Usamos map, aplicando uma função no valor de retorno da função empacotada para transformar a em a .Reader[SocialNetworkConnection, Seq[User]]
Reader[SocialNetworkConnection, Int]

flatMapé semelhante a map, mas é usado para “mesclar” resultados que são Readers. Novamente, é um operador comum em scala e programação funcional e as classes de coleção fornecem um bom exemplo: imagine que você tem uma sequência Stringque deseja dividir em torno do espaço e deseja retornar uma lista de todos os tokens extraídos desta forma, se você usa mappara aplicar splitem cada elemento desta sequência, você terá uma sequência de sequências. O que você quer é uma sequência “achatada” de String:

val strSeq = Seq("string 1", "another string", "yet another string")
strSeq
.flatMap(_.split('_'))

flatMapmescla as coleções que recebe de cada chamada splitpara retornar apenas uma sequência. Da mesma forma, flatMapem Readerpermite alterar a função agrupada mesclando um Leitor com o Leitor retornado por outra função.

O objeto ReaderMonadfornece um puremétodo que permite criar um leitor quando você tem um resultado que não depende de uma conexão, mas você precisa retornar um leitor de qualquer maneira.

Observe também que, por Readerter as funções mape flatMap, o scala é capaz de entender seu uso em para compreensões .

Configure a conexão

Então, todo esse bootstrapping de mônadas e tipos de retorno engraçados é interessante, mas como isso o ajuda a configurar uma conexão concreta no Play? Em algum ponto, você terá que substituir a classe Reader por um resultado real (provavelmente no topo de sua cadeia de chamadas, quando estiver retornando um resultado para uma solicitação http).

Então, normalmente, com o Play 2 , você terá criado uma implementação SQL / Anorm de sua conexão, praticamente o que declaramos anteriormente:

class MySQLConnection(dbName: String) extends UserConnection with SocialNetworkConnection {

def findAllUsers: Seq[User] = DB.withConnection(dbName) { }
def createUser(u: User): Either[Throwable, User] = DB.withConnection(dbName) { }
def getFriends(u: User): Seq[User] = DB.withConnection(dbName) { }

}

Agora você pode declarar em seu controlador que está usando esta implementação:

object Application extends Controller {

val sqlConnection
= new MySQLConnection("default");

def allEmails = Action {
// get a reader with the result we want
val resultReader
= User.findAll.map{ allUsers =>
Ok(Json.toJson(allUsers.map(_.email)))
}
//apply the function wrapped in the reader to get the final result
resultReader
(sqlConnection)
}

}

A configuração é basicamente a mesma que para os implícitos, exceto que você mantém tudo explícito. Acho isso muito melhor, pois está claro, pelas assinaturas de função e os tipos de retorno, o que está acontecendo em sua cadeia de chamadas. Qualquer pessoa (para a qual você explica o que o tipo de leitor representa) pode ver quais funções em seu código dependem de qual tipo de conexão, e não precisa adivinhar onde o implícito é introduzido no contexto, você só precisa olhar no topo da cadeia de chamadas para ver onde o Reader é finalmente aplicado.

Um ultimo truque

Você pode dizer que o último não é totalmente transparente para quem não está acostumado com Leitores. Em sua palestra, Runar Oli fornece uma boa solução para isso, para tornar o que está acontecendo ainda mais explícito e ter uma forma semelhante à construção da API Play, é apenas mais um truque de embrulho:resultReader(sqlConnection)DB.withConnection

object MySQLConnection {

def withUserConnection[To](dbName: String)(f: Reader[UserConnection, To]) {
val sqlConnection
= new MySQLConnection(dbName);
f
(sqlConnection)
}

}

Você então faz:

def allEmails = Action {
MySQLConnection.withUserConnection("default") {
User.findAll.map{ allUsers =>
Ok(Json.toJson(allUsers.map(_.email)))
}
}
}

O código agora está muito mais limpo e, para qualquer revisor de código desavisado, não é muito importante entender o que está acontecendo com o Reader, é claro pelo que está escrito que um UserConnectioné usado dentro desse bloco e que sua implementação vem de MySQLConnection.

No Play, cuida de toda a configuração da conexão e de fechá-la quando terminar. Mas, no caso em que você está usando outro tipo de provedor e precisa fazer tudo isso sozinho, pode mover esse processo para a função, certificando-se de que todo o bootstrap e a limpeza sejam cuidados ao usar uma conexão.DB.withConnectionwithUserConnection

Obrigado Runar Oli e scala

Como mencionado anteriormente, a maior parte do que é explicado aqui é fortemente inspirado pela apresentação Dead-Simple Dependency Injection de Runar Oli no 2012 neScala. Você pode encontrar a maioria dessas ferramentas na biblioteca scalaz e tudo isso é possível graças ao bom poder do scala.

Espero que esses dois posts tenham sido úteis para você e que tenham ajudado a levá-lo a um “Doh!” momento semelhante ao que tive quando estava assistindo a palestra de Runar Oli. Se você tiver alguma dúvida, não hesite.