Monada de lector para Inyección de dependencias: dependencias múltiples, llamadas anidadas


Cuando se le pregunta sobre la Inyección de dependencias en Scala, muchas respuestas apuntan a usar la Mónada Reader, ya sea la de Scalaz o simplemente la tuya. Hay una serie de artículos muy claros que describen los fundamentos del enfoque (por ejemplo, La charla de Runar, Jason blog ), pero no logré encontrar un ejemplo más completo, y no veo las ventajas de ese enfoque sobre, por ejemplo, un DI "manual" más tradicional (ver la guía que escribí). Mas probablemente me estoy perdiendo algún punto importante, de ahí la pregunta.

A modo de ejemplo, imaginemos que tenemos estas clases: {[16]]}

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

class FindUsers(datastore: Datastore) {
  def inactive(): Unit = ()
}

class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
  def emailInactive(): Unit = ()
}

class CustomerRelations(userReminder: UserReminder) {
  def retainUsers(): Unit = {}
}

Aquí estoy modelando cosas usando clases y parámetros de constructor, lo que juega muy bien con los enfoques DI "tradicionales", sin embargo, este diseño tiene un par de buenos lados:

  • cada funcionalidad tiene dependencias claramente enumeradas. Asumimos que las dependencias son realmente necesarias para que la funcionalidad funcione correctamente
  • las dependencias están ocultas entre funcionalidades, por ejemplo, UserReminder no tiene idea de que FindUsers necesita un almacén de datos. Las funcionalidades pueden ser incluso en unidades de compilación separadas
  • estamos usando solo Scala pura; las implementaciones pueden aprovechar clases inmutables, funciones de orden superior, los métodos de "lógica de negocios" pueden devolver valores envueltos en la mónada IO si queremos capturar los efectos, etc.

¿Cómo se podría modelar esto con la mónada Lectora? Sería es bueno conservar las características anteriores, para que quede claro qué tipo de dependencias necesita cada funcionalidad, y ocultar las dependencias de una funcionalidad de otra. Tenga en cuenta que usar classes es más un detalle de implementación; tal vez la solución "correcta" usando la mónada del Lector usaría algo más.

Encontré una pregunta algo relacionada que sugiere: {[16]]}

  • usando un único objeto de entorno con todas las dependencias
  • utilizando entornos locales
  • patrón" parfait "
  • mapas indexados por tipos

Sin embargo, aparte de ser (pero eso es subjetivo) un poco demasiado complejo como para una cosa tan simple, en todas estas soluciones, por ejemplo, el método retainUsers (que llama a emailInactive, que llama a inactive para encontrar a los usuarios inactivos) necesitaría saber sobre la dependencia Datastore, para poder llamar correctamente a las funciones anidadas, ¿o me equivoco?

En qué aspectos utilizaría la Mónada Lector para tal " negocio aplicación " ¿ser mejor que solo usar parámetros de constructor?

Author: Community, 2015-03-20

2 answers

Cómo modelar este ejemplo

¿Cómo se podría modelar esto con la mónada Lectora?

No estoy seguro de si esto debe ser modelado con el Lector, sin embargo, puede ser por:

  1. codificando las clases como funciones lo que hace que el código se reproduzca mejor con Reader
  2. componiendo las funciones con Reader en a para comprensión y usándolo

Justo antes del inicio necesito contarte sobre pequeños ajustes de código de muestra que me sentí beneficioso para esta respuesta. El primer cambio es sobre el método FindUsers.inactive. Dejo que devuelva List[String] para que la lista de direcciones se pueda utilizar en el método UserReminder.emailInactive. También he agregado implementaciones simples a los métodos. Finalmente, la muestra utilizará un siguiente versión enrollada a mano de Reader monad:

case class Reader[Conf, T](read: Conf => T) { self =>

  def map[U](convert: T => U): Reader[Conf, U] =
    Reader(self.read andThen convert)

  def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
    Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))

  def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
    Reader[BiggerConf, T](extractFrom andThen self.read)
}

object Reader {
  def pure[C, A](a: A): Reader[C, A] =
    Reader(_ => a)

  implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
    Reader(read)
}

Modelling step 1. Codificación de clases como funciones

Tal vez eso sea opcional, no estoy seguro, pero más tarde hace que la comprensión se vea mejor. Tenga en cuenta que la función resultante es curry. Se también toma el(los) argumento (s) del constructor anterior (s) como su primer parámetro (lista de parámetros). De esa manera

class Foo(dep: Dep) {
  def bar(arg: Arg): Res = ???
}
// usage: val result = new Foo(dependency).bar(arg)

Se convierte en

object Foo {
  def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)

Tenga en cuenta que cada uno de Dep, Arg, Res los tipos pueden ser completamente arbitrarios: una tupla, una función o un tipo simple.

Aquí está el código de ejemplo después de los ajustes iniciales, transformado en funciones:

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

object FindUsers {
  def inactive: Datastore => () => List[String] =
    dataStore => () => dataStore.runQuery("select inactive")
}

object UserReminder {
  def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
    emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}

object CustomerRelations {
  def retainUsers(emailInactive: () => Unit): () => Unit =
    () => {
      println("emailing inactive users")
      emailInactive()
    }
}

Una cosa a notar aquí es que las funciones particulares no dependen de los objetos completos, sino solo de los utilizados directamente parte. Donde en la versión OOP UserReminder.emailInactive() la instancia llamaría userFinder.inactive() aquí solo llama inactive() - una función que se le pasa en el primer parámetro.

Tenga en cuenta que el código exhibe las tres propiedades deseables de la pregunta:

  1. está claro qué tipo de dependencias necesita cada funcionalidad
  2. oculta dependencias de una funcionalidad de otra
  3. retainUsers el método no debería necesitar saber acerca de la dependencia del almacén de datos

Modelización paso 2. Usando el Lector para componer funciones y ejecutarlas

Reader monad solo le permite componer funciones que dependen del mismo tipo. Esto a menudo no es un caso. En nuestro ejemplo FindUsers.inactive depende de Datastore y UserReminder.emailInactive de EmailServer. Para resolver ese problema uno podría introducir un nuevo tipo (a menudo referido como Config) que contiene todas las dependencias, luego cambiar las funciones por lo que todos dependen de ella y solo toman de ella los datos relevantes. Eso obviamente está mal desde la dependencia perspectiva de gestión porque de esa manera haces que estas funciones también dependan en tipos que no deberían saber en primer lugar.

Afortunadamente, resulta que existe una manera de hacer que la función funcione con Config incluso si acepta solo una parte de ella como parámetro. Es un método llamado local, definido en Reader. Debe estar provista de una forma de extraer la parte pertinente del Config.

Este conocimiento aplicado al ejemplo en el aspecto así:

object Main extends App {

  case class Config(dataStore: Datastore, emailServer: EmailServer)

  val config = Config(
    new Datastore { def runQuery(query: String) = List("[email protected]") },
    new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
  )

  import Reader._

  val reader = for {
    getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
    emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
    retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
  } yield retainUsers

  reader.read(config)()

}

Ventajas sobre el uso de parámetros de constructor

¿En qué aspectos sería mejor usar la Mónada Reader para tal "aplicación de negocios" que solo usar parámetros de constructor?

Espero que al preparar esta respuesta me haya hecho más fácil juzgar por ti mismo en qué aspectos vencería a los constructores simples. Sin embargo, si tuviera que enumerar estos, aquí está mi lista. Descargo de responsabilidad: Tengo antecedentes OOP y no puedo apreciar Lector y Kleisli totalmente como yo no los uso.

  1. Uniformidad - no importa lo corto / largo que sea la comprensión de for, es solo un Lector y se puede componer fácilmente con otro instancia, tal vez solo introduciendo un tipo de configuración más y rociando algunas llamadas local encima de él. Este punto es IMO más bien una cuestión de gusto, porque cuando usas constructores nadie te impide componer lo que quieras, a menos que alguien haga algo estúpido, como hacer trabajo en constructor que es considerado una mala práctica en OOP.
  2. Reader es una mónada, por lo que obtiene todos los beneficios relacionados con eso - sequence, traverse métodos implementados de forma gratuita.
  3. En algunos casos puede ser preferible construir el Lector solo una vez y usarlo para una amplia gama de configuraciones. Con constructores nadie te impide hacer eso, solo necesitas construir el gráfico de objetos completo de nuevo para cada configuración entrante. Si bien no tengo ningún problema con eso (incluso prefiero hacerlo en cada solicitud a solicitud), se no una idea obvia para mucha gente por razones que solo puedo especular.
  4. Reader te empuja hacia el uso de funciones más, que se reproducirán mejor con la aplicación escrita predominantemente en estilo FP.
  5. Reader separa las preocupaciones; puede crear, interactuar con todo, definir la lógica sin proporcionar dependencias. En realidad suministrar más tarde, por separado. (Gracias Ken Scrambler por este punto). Esto se oye a menudo ventaja de lector, sin embargo, eso también es posible con llano constructor.

También me gustaría decir lo que no me gusta en Reader.

  1. Marketing. A veces me da la impresión, que el Lector se comercializa para todo tipo de dependencias, sin distinción si eso es una cookie de sesión o una base de datos. Para mí no tiene mucho sentido usar Reader para objetos prácticamente constantes, como el correo electrónico servidor o repositorio de este ejemplo. Para tales dependencias encuentro constructores simples y / o funciones parcialmente aplicadas mucho mejor. Esencialmente Reader le da flexibilidad para que pueda especificar sus dependencias en cada llamada, pero si realmente no lo necesitas, solo pagas sus impuestos.
  2. Pesadez implícita - usar Reader sin implicits haría el ejemplo difícil de leer. Por otro lado, cuando te escondes las partes ruidosas que usan implicits y cometen algún error, el compilador a veces le dará mensajes difíciles de descifrar.
  3. Ceremonia con pure, local y creando clases de configuración propias / usando tuplas para eso. Fuerzas del lector usted para agregar un poco de código eso no se trata del dominio del problema, por lo tanto, introduciendo algo de ruido en el código. Por otro lado, una aplicación que utiliza constructores a menudo utiliza patrón de fábrica, que también es de fuera del dominio del problema, por lo que esta debilidad no es que grave.

¿Qué pasa si no quiero convertir mis clases a objetos con funciones?

Quieres. Técnicamente puede evitar eso, pero solo mire lo que pasaría si no convirtiera la clase FindUsers a objeto. La línea respectiva de for comprehension se vería como:

getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)

Que no es tan legible, ¿verdad? El punto es que Reader opera en funciones, por lo que si no las tienes ya, necesitas construirlas en línea, lo que a menudo no es tan bonito.

 32
Author: Przemek Pokrywka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-19 15:49:39

Creo que la principal diferencia es que en su ejemplo está inyectando todas las dependencias cuando se instancian objetos. La mónada Lector básicamente construye una funciones cada vez más complejas para llamar dadas las dependencias, que luego se devuelven a las capas más altas. En este caso, la inyección ocurre cuando finalmente se llama a la función.

Una ventaja inmediata es la flexibilidad, especialmente si puede construir su mónada una vez y luego desea usarla con diferentes inyecciones dependencia. Una desventaja es, como usted dice, potencialmente menos claridad. En ambos casos, la capa intermedia solo necesita saber acerca de sus dependencias inmediatas, por lo que ambos funcionan como se anuncia para DI.

 2
Author: Daniel Langdon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-16 14:16:05