scala dependency-injection implicits

Inyección de dependencia Scala: alternativas a parámetros implícitos



dependency-injection implicits (5)

Creo que la inyección de dependencia de ascensor hace lo que quieres. Vea la wiki para detalles usando el método doWith ().

Tenga en cuenta que puede usarlo como una biblioteca separada, incluso si no está ejecutando lift.

Por favor, perdone la longitud de esta pregunta.

A menudo necesito crear alguna información contextual en una capa de mi código y consumir esa información en otra parte. Generalmente me encuentro usando parámetros implícitos:

def foo(params)(implicit cx: MyContextType) = ... implicit val context = makeContext() foo(params)

Esto funciona, pero requiere que el parámetro implícito se transmita mucho, contaminando las firmas del método de capa después del diseño de las funciones intermedias, incluso si no les importa.

def foo(params)(implicit cx: MyContextType) = ... bar() ... def bar(params)(implicit cx: MyContextType) = ... qux() ... def qux(params)(implicit cx: MyContextType) = ... ged() ... def ged(params)(implicit cx: MyContextType) = ... mog() ... def mog(params)(implicit cx: MyContextType) = cx.doStuff(params) implicit val context = makeContext() foo(params)

Encuentro este enfoque feo, pero tiene una ventaja: es seguro. Sé con certeza que mog recibirá un objeto de contexto del tipo correcto, o no compilaría.

Aliviaría el desorden si pudiera usar alguna forma de "inyección de dependencia" para ubicar el contexto relevante. Las citas están ahí para indicar que esto es diferente de los patrones de inyección de dependencia habituales que se encuentran en Scala.

El punto de inicio foo y el punto final mog pueden existir en niveles muy diferentes del sistema. Por ejemplo, foo podría ser un controlador de inicio de sesión de usuario y mog podría estar haciendo acceso a SQL. Puede haber muchos usuarios conectados al mismo tiempo, pero solo hay una instancia de la capa SQL. Cada vez que un usuario diferente llama a mog , se necesita un contexto diferente. Por lo tanto, el contexto no se puede incluir en el objeto receptor, ni tampoco desea combinar las dos capas de ninguna manera (como el Patrón de pastel). También prefiero no confiar en una biblioteca DI / IoC como Guice o Spring. Los he encontrado muy pesados ​​y no muy adecuados para Scala.

Entonces, lo que creo que necesito es algo que permita a mog recuperar el objeto de contexto correcto para él en tiempo de ejecución, un poco como un ThreadLocal con una pila en él:

def foo(params) = ...bar()... def bar(params) = ...qux()... def qux(params) = ...ged()... def ged(params) = ...mog()... def mog(params) = { val cx = retrieveContext(); cx.doStuff(params) } val context = makeContext() usingContext(context) { foo(params) }

Pero eso caería tan pronto como el actor asíncrono estuviera involucrado en cualquier parte de la cadena. No importa qué biblioteca de actores utilice, si el código se ejecuta en un subproceso diferente, perderá ThreadLocal .

Entonces ... ¿hay un truco que me estoy perdiendo? ¿Una forma de pasar información de manera contextual en Scala que no contamine las firmas de los métodos intermedios, que no incorpore el contexto en el receptor de forma estática y que aún es segura para el tipo?


De manera similar al enfoque implícito, con Macros de Scala puede hacer el cableado automático de objetos utilizando constructores; vea mi proyecto MacWire (y disculpe la autopromoción).

MacWire también tiene ámbitos (bastante personalizable, se proporciona una implementación de ThreadLocal ). Sin embargo, no creo que pueda propagar el contexto a través de llamadas de actores con una biblioteca, debe llevar algún identificador. Esto puede ser, por ejemplo, a través de un contenedor para enviar mensajes de actor, o más directamente con el mensaje.

Luego, siempre que el identificador sea único por solicitud / sesión / sea cual sea su alcance, es solo una cuestión de buscar cosas en un mapa a través de un proxy (como hacen los ámbitos MacWire, el "identificador" aquí no es necesario ya que se almacena en el ThreadLocal ).


La biblioteca estándar de Scala incluye algo como su hipotético "usingContext" llamado DynamicVariable. Esta pregunta tiene alguna información al respecto. ¿ Cuándo debemos usar scala.util.DynamicVariable? . DynamicVariable usa un ThreadLocal debajo del capó, por lo que muchos de sus problemas con ThreadLocal permanecerán.

La mónada del lector es una alternativa funcional para pasar explícitamente un entorno http://debasishg.blogspot.com/2010/12/case-study-of-cleaner-composition-of.html . La mónada del lector se puede encontrar en Scalaz http://code.google.com/p/scalaz/ . Sin embargo, ReaderMonad "contamina" sus firmas en el sentido de que sus tipos deben cambiar y, en general, la programación monádica puede provocar una gran reestructuración en su código, y las asignaciones de objetos adicionales para todos los cierres pueden no funcionar bien si el rendimiento o la memoria son una preocupación.

Ninguna de estas técnicas compartirá automáticamente un contexto sobre el envío de un mensaje del actor.


Un poco tarde para la fiesta, ¿pero ha considerado usar parámetros implícitos para los constructores de sus clases?

class Foo(implicit biz:Biz) { def f() = biz.doStuff } class Biz { def doStuff = println("do stuff called") }

Si quisiera tener un nuevo biz para cada llamada a f() , podría permitir que el parámetro implícito sea una función que devuelva un nuevo biz:

class Foo(implicit biz:() => Biz) { def f() = biz().doStuff }

Ahora simplemente necesitas proporcionar el contexto al construir Foo . Que puedes hacer así:

trait Context { private implicit def biz = () => new Biz implicit def foo = new Foo // The implicit parameter biz will be resolved to the biz method above } class UI extends Context { def render = foo.f() }

Tenga en cuenta que el método biz implícito no será visible en la UI de UI . Así que básicamente ocultamos esos detalles :)

Escribí una publicación en el blog sobre el uso de parámetros implícitos para la inyección de dependencia que se puede encontrar aquí (autopromoción descarada;))


Usted preguntó esto hace casi un año, pero aquí hay otra posibilidad. Si solo necesitas llamar a un método:

def fooWithContext(cx: MyContextType)(params){ def bar(params) = ... qux() ... def qux(params) = ... ged() ... def ged(params) = ... mog() ... def mog(params) = cx.doStuff(params) ... bar() ... } fooWithContext(makeContext())(params)

Si necesita que todos los métodos sean visibles externamente:

case class Contextual(cx: MyContextType){ def foo(params) = ... bar() ... def bar(params) = ... qux() ... def qux(params) = ... ged() ... def ged(params) = ... mog() ... def mog(params) = cx.doStuff(params) } Contextual(makeContext()).foo(params)

Este es básicamente el patrón de la torta, excepto que si todas tus cosas encajan en un solo archivo, no necesitas todas las cosas desordenadas para combinarlas en un solo objeto: simplemente puedes anidarlas. Hacerlo de esta manera también hace que cx esté apropiadamente en el ámbito léxico, de modo que no termines con un comportamiento divertido cuando usas futuros, actores y demás. Sospecho que si usa el nuevo AnyVal, podría incluso eliminar la sobrecarga de asignar el objeto Contextual .

Si desea dividir sus cosas en múltiples archivos utilizando trait , solo necesita un trait por archivo para mantener todo y poner el MyContextType en el ámbito adecuado, si no necesita el elemento reemplazable y elegante de herencia. La mayoría de los ejemplos de patrón de pastel tienen.

// file1.scala case class Contextual(cx: MyContextType) with Trait1 with Trait2{ def foo(params) = ... bar() ... def bar(params) = ... qux() ... } // file2.scala trait Trait1{ self: Contextual => def qux(params) = ... ged() ... def ged(params) = ... mog() ... } // file3.scala trait Trait2{ self: Contextual => def mog(params) = cx.doStuff(params) } // file4.scala Contextual(makeContext()).foo(params)

Se ve un poco desordenado en un pequeño ejemplo, pero recuerde que solo necesita dividirlo en un nuevo rasgo si el código se está volviendo demasiado grande para estar cómodo en un archivo. Para ese momento, sus archivos son razonablemente grandes, por lo que un extra de 2 líneas de repetición en un archivo de 200-500 líneas no es realmente tan malo.

EDITAR:

Esto funciona también con cosas asíncronas.

case class Contextual(cx: MyContextType){ def foo(params) = ... bar() ... def bar(params) = ... qux() ... def qux(params) = ... ged() ... def ged(params) = ... mog() ... def mog(params) = Future{ cx.doStuff(params) } def mog2(params) = (0 to 100).par.map(x => x * cx.getSomeValue ) def mog3(params) = Props(new MyActor(cx.getSomeValue)) } Contextual(makeContext()).foo(params)

Simplemente funciona utilizando la anidación. Me impresionaría si pudiera obtener una funcionalidad similar trabajando con DynamicVariable .

Necesitaría una subclase especial de Future que almacene el DynamicVariable.value actual cuando se cree, y enganche en el método prepare() o execute() ExecutionContext para extraer el value y configurar adecuadamente DynamicVariable antes de ejecutar el Future .

Luego necesitaría un scala.collection.parallel.TaskSupport especial para hacer algo similar para que funcionen las colecciones paralelas. Y un especial akka.actor.Props para hacer algo similar para eso .

Cada vez que hay un nuevo mecanismo de creación de tareas asíncronas, las implementaciones basadas en DynamicVariable se romperán y tendrás errores extraños en los que terminarás tirando del Context incorrecto. Cada vez que agregue una nueva DynamicVariable para realizar un seguimiento, deberá aplicar un parche a todos sus ejecutores especiales para configurar / DynamicVariable adecuadamente esta nueva DynamicVariable . Con el anidamiento, puede dejar que el cierre lexical se encargue de todo esto por usted.

(Creo que Future s, collections.parallel y Prop s cuentan como "capas intermedias que no son mi código")