unity patron manejo inyeccion framework dependencias c# .net dependency-injection ioc-container strategy-pattern

patron - manejo de dependencias c#



Inyección de dependencia y el patrón de estrategia (4)

Hay una enorme cantidad de discusión sobre este tema, pero todos parecen perder una respuesta obvia. Me gustaría ayudar a investigar esta solución de contenedor IOC "obvia". Las diversas conversaciones asumen la selección de estrategias en tiempo de ejecución y el uso de un contenedor de IOC. Continuaré con estas suposiciones.

También quiero agregar la suposición de que no se debe seleccionar una sola estrategia. Más bien, podría necesitar recuperar un objeto-gráfico que tenga varias estrategias encontradas a lo largo de los nodos del gráfico.

Primero esbozaré rápidamente las dos soluciones comúnmente propuestas, y luego presentaré la alternativa "obvia" que me gustaría ver con el apoyo de un contenedor IOC. Utilizaré Unity como sintaxis de ejemplo, aunque mi pregunta no es específica de Unity.

Enlaces nombrados

Este enfoque requiere que cada nueva estrategia tenga un enlace agregado manualmente:

Container.RegisterType<IDataAccess, DefaultAccessor>(); Container.RegisterType<IDataAccess, AlphaAccessor>("Alpha"); Container.RegisterType<IDataAccess, BetaAccessor>("Beta");

... y luego se solicita explícitamente la estrategia correcta:

var strategy = Container.Resolve<IDataAccess>("Alpha");

  • Pros: simple y compatible con todos los contenedores del COI
  • Contras:
    • Normalmente vincula al llamante al Contenedor del COI, y sin duda requiere que la persona que llama sepa algo acerca de la estrategia (como el nombre "Alfa").
    • Cada nueva estrategia debe agregarse manualmente a la lista de enlaces.
    • Este enfoque no es adecuado para manejar estrategias múltiples en un gráfico de objetos. En resumen, no cumple con los requisitos.

Fábrica abstracta

Para ilustrar este enfoque, asuma las siguientes clases:

public class DataAccessFactory{ public IDataAccess Create(string strategy){ return //insert appropriate creation logic here. } public IDataAccess Create(){ return //Choose strategy through ambient context, such as thread-local-storage. } } public class Consumer { public Consumer(DataAccessFactory datafactory) { //variation #1. Not sufficient to meet requirements. var myDataStrategy = datafactory.Create("Alpha"); //variation #2. This is sufficient for requirements. var myDataStrategy = datafactory.Create(); } }

El contenedor IOC tiene entonces la siguiente vinculación:

Container.RegisterType<DataAccessFactory>();

  • Pros:
    • El contenedor del COI está oculto a los consumidores
    • El "contexto ambiental" está más cerca del resultado deseado, pero ...
  • Contras:
    • Los constructores de cada estrategia pueden tener diferentes necesidades. Pero ahora la responsabilidad de la inyección del constructor se ha transferido a la fábrica abstracta desde el contenedor. En otras palabras, cada vez que se agrega una nueva estrategia, puede ser necesario modificar la fábrica abstracta correspondiente.
    • El uso intensivo de estrategias significa grandes cantidades de creación de fábricas abstractas. Sería bueno que el contenedor del COI simplemente diera un poco más de ayuda.
    • Si se trata de una aplicación de subprocesos múltiples y el "contexto ambiental" sí lo proporciona el almacenamiento local de subprocesos, para cuando un objeto está utilizando una fábrica abstracta inyectada para crear el tipo que necesita, puede estar funcionando en una hilo diferente que ya no tiene acceso al valor necesario de almacenamiento local de subprocesos.

Tipo de conmutación / enlace dinámico

Este es el enfoque que quiero usar en lugar de los dos enfoques anteriores. Implica proporcionar un delegado como parte del enlace contenedor de IOC. La mayoría de todos los Contenedores de COI ya tienen esta habilidad, pero este enfoque específico tiene una diferencia sutil importante.

La sintaxis sería algo como esto:

Container.RegisterType(typeof(IDataAccess), new InjectionStrategy((c) => { //Access ambient context (pehaps thread-local-storage) to determine //the type of the strategy... Type selectedStrategy = ...; return selectedStrategy; }) );

Observe que InjectionFactory no está devolviendo una instancia de IDataAccess . En su lugar, devuelve una descripción de tipo que implementa IDataAccess . El Contenedor de IOC realizaría la creación habitual y la "creación" de ese tipo, lo que podría incluir la selección de otras estrategias.

Esto está en contraste con el enlace estándar de tipo a delegado que, en el caso de Unity, se codifica de esta manera:

Container.RegisterType(typeof(IDataAccess), new InjectionFactory((c) => { //Access ambient context (pehaps thread-local-storage) to determine //the type of the strategy... IDataAccess instanceOfelectedStrategy = ...; return instanceOfelectedStrategy; }) );

Lo anterior en realidad está cerca de satisfacer la necesidad general, pero definitivamente se queda corto con la hipotética Unity InjectionStrategy .

Centrándose en la primera muestra (que utilizó una hipotética InjectionStrategy Unity):

  • Pros:
    • Oculta el contenedor
    • No es necesario crear fábricas abstractas interminables ni hacer que los consumidores toquen con ellas.
    • No es necesario ajustar manualmente los enlaces de contenedores de IOC siempre que haya una nueva estrategia disponible.
    • Permite que el contenedor retenga controles de administración de por vida.
    • Admite una historia DI pura, lo que significa que una aplicación de subprocesos múltiples puede crear todo el objeto-gráfico en una secuencia con la configuración adecuada de almacenamiento local de subprocesos.
  • Contras:
    • Debido a que el Type devuelto por la estrategia no estaba disponible cuando se crearon los enlaces de contenedor IOC iniciales, significa que puede haber un pequeño golpe de rendimiento la primera vez que se devuelve ese tipo. En otras palabras, el contenedor debe reflejar en el lugar el tipo para descubrir qué constructores tiene, para que sepa cómo inyectarlo. Todas las instancias posteriores de ese tipo deben ser rápidas, porque el contenedor puede almacenar en caché los resultados que encontró desde la primera vez. Esto no es una "estafa" que valga la pena mencionar, pero estoy intentando divulgarlo por completo.
    • ???

¿Hay un contenedor de COI existente que pueda comportarse de esta manera? ¿Alguien tiene una clase de inyección personalizada Unity que logra este efecto?


He logrado este requisito de muchas formas en los últimos años. Primero, saquemos los puntos principales que puedo ver en su publicación

asume la selección de estrategias en tiempo de ejecución y el uso de un contenedor de IOC ... agrega la suposición de que no se debe seleccionar una sola estrategia. Más bien, podría necesitar recuperar un objeto-gráfico que tiene varias estrategias ... [no] debe vincular a la persona que llama con el Contenedor de IOC ... Cada nueva estrategia debe [no necesitar] agregarse manualmente a la lista de enlaces. .. Sería bueno si el contenedor del COI simplemente brindara un poco más de ayuda.

He utilizado Simple Injector como mi contenedor de elección desde hace un tiempo y uno de los factores que influyen en esta decisión es que cuenta con un amplio soporte para genéricos. Es a través de esta función que implementaremos sus requisitos.

Soy un firme creyente de que el código debería hablar por sí mismo, así que voy a entrar directamente ...

  • Definí una clase adicional ContainerResolvedClass<T> para demostrar que Simple Injector encuentra las implementaciones correctas y las inyecta con éxito en un constructor. Esa es la única razón para la clase ContainerResolvedClass<T> . (Esta clase expone los manejadores que se inyectan para fines de prueba a través de result.Handlers ).

Esta primera prueba requiere que obtengamos una implementación para la clase ficticia Type1 :

[Test] public void CompositeHandlerForType1_Resolves_WithAlphaHandler() { var container = this.ContainerFactory(); var result = container.GetInstance<ContainerResolvedClass<Type1>>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(1)); Assert.That(handlers.Contains(typeof(AlphaHandler<Type1>)), Is.True); }

Esta segunda prueba requiere que recuperemos una implementación para la clase ficticia Type2 :

[Test] public void CompositeHandlerForType2_Resolves_WithAlphaHandler() { var container = this.ContainerFactory(); var result = container.GetInstance<ContainerResolvedClass<Type2>>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(1)); Assert.That(handlers.Contains(typeof(BetaHandler<Type2>)), Is.True); }

Esta tercera prueba requiere que obtengamos dos implementaciones para la clase de ficción Type3 :

[Test] public void CompositeHandlerForType3_Resolves_WithAlphaAndBetaHandlers() { var container = this.ContainerFactory(); var result = container.GetInstance<ContainerResolvedClass<Type3>>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(2)); Assert.That(handlers.Contains(typeof(AlphaHandler<Type3>)), Is.True); Assert.That(handlers.Contains(typeof(BetaHandler<Type3>)), Is.True); }

Estas pruebas parecen cumplir con sus requisitos y lo mejor de todo es que no se daña ningún contenedor en la solución .

El truco es usar una combinación de objetos de parámetros e interfaces de marcadores. Los objetos de parámetro contienen los datos para el comportamiento (es decir, los de IHandler ) y las interfaces de marcador determinan qué comportamientos actúan sobre qué objetos de parámetro.

Aquí están las interfaces de marcadores y los objetos de parámetros: Type3 que Type3 está marcado con ambas interfaces de marcadores:

private interface IAlpha { } private interface IBeta { } private class Type1 : IAlpha { } private class Type2 : IBeta { } private class Type3 : IAlpha, IBeta { }

Estos son los comportamientos ( IHandler<T> ''s):

private interface IHandler<T> { } private class AlphaHandler<TAlpha> : IHandler<TAlpha> where TAlpha : IAlpha { } private class BetaHandler<TBeta> : IHandler<TBeta> where TBeta : IBeta { }

Este es el único método que encontrará todas las implementaciones de un genérico abierto:

public IEnumerable<Type> GetLoadedOpenGenericImplementations(Type type) { var types = from assembly in AppDomain.CurrentDomain.GetAssemblies() from t in assembly.GetTypes() where !t.IsAbstract from i in t.GetInterfaces() where i.IsGenericType where i.GetGenericTypeDefinition() == type select t; return types; }

Y este es el código que configura el contenedor para nuestras pruebas:

private Container ContainerFactory() { var container = new Container(); var types = this.GetLoadedOpenGenericImplementations(typeof(IHandler<>)); container.RegisterAllOpenGeneric(typeof(IHandler<>), types); container.RegisterOpenGeneric( typeof(ContainerResolvedClass<>), typeof(ContainerResolvedClass<>)); return container; }

Y finalmente, la clase de prueba ContainerResolvedClass<>

private class ContainerResolvedClass<T> { public readonly IEnumerable<IHandler<T>> Handlers; public ContainerResolvedClass(IEnumerable<IHandler<T>> handlers) { this.Handlers = handlers; } }

Me doy cuenta de que esta publicación es bastante larga, pero espero que demuestre claramente una posible solución a su problema ...


Por lo general, uso una combinación de las opciones de Abstract Factory y Bindings con nombre . Después de probar muchos enfoques diferentes, encuentro que este enfoque es un equilibrio decente.

Lo que hago es crear una fábrica que esencialmente envuelve la instancia del contenedor. Vea la sección en el artículo de Mark llamado fábrica basada en contenedores . Como sugiere, hago que esta fábrica forme parte de la raíz de la composición.

Para que mi código sea un poco más limpio y menos basado en "cadenas mágicas", utilizo una enumeración para denotar las diferentes estrategias posibles, y uso el método .ToString () para registrar y resolver.

De tus contras de estos enfoques:

Normalmente vincula al llamante al contenedor del COI

En este enfoque, el contenedor se referencia en la fábrica, que es parte de la raíz de composición, por lo que ya no es un problema (en mi opinión).

. . . y ciertamente requiere que la persona que llama sepa algo sobre la estrategia (como el nombre "Alpha").

Cada nueva estrategia debe agregarse manualmente a la lista de enlaces. Este enfoque no es adecuado para manejar estrategias múltiples en un gráfico de objetos. En resumen, no cumple con los requisitos.

En algún punto, el código debe escribirse para reconocer el mapeo entre la estructura que proporciona la implementación (contenedor, proveedor, fábrica, etc.) y el código que lo requiere. No creo que pueda evitar esto a menos que quiera usar algo que esté basado puramente en convenciones.

Los constructores de cada estrategia pueden tener diferentes necesidades. Pero ahora la responsabilidad de la inyección del constructor se ha transferido a la fábrica abstracta desde el contenedor. En otras palabras, cada vez que se agrega una nueva estrategia, puede ser necesario modificar la fábrica abstracta correspondiente.

Este enfoque resuelve esta preocupación por completo.

El uso intensivo de estrategias significa grandes cantidades de creación de fábricas abstractas. [...]

Sí, necesitarás una fábrica abstracta para cada conjunto de estrategias.

Si se trata de una aplicación de subprocesos múltiples y el "contexto ambiental" sí lo proporciona el almacenamiento local de subprocesos, para cuando un objeto está utilizando una fábrica abstracta inyectada para crear el tipo que necesita, puede estar funcionando en una hilo diferente que ya no tiene acceso al valor necesario de almacenamiento local de subprocesos.

Esto ya no será un problema ya que no se usará TLC.

No creo que haya una solución perfecta, pero este enfoque me ha funcionado bien.


Por lo que puedo decir, esta pregunta es acerca de la selección en tiempo de ejecución o mapeo de una de varias estrategias candidatas.

No hay ninguna razón para confiar en un Contenedor DI para hacer esto, ya que hay al menos tres formas de hacerlo de una manera independiente del contenedor:

Mi preferencia personal es la sugerencia de rol de nombre de tipo parcial.


Esta es una respuesta tardía, pero tal vez ayudará a otros.

Tengo un enfoque bastante simple. Simplemente creo un StrategyResolver para que no dependa directamente de Unity.

public class StrategyResolver : IStrategyResolver { private IUnityContainer container; public StrategyResolver(IUnityContainer unityContainer) { this.container = unityContainer; } public T Resolve<T>(string namedStrategy) { return this.container.Resolve<T>(namedStrategy); } }

Uso:

public class SomeClass: ISomeInterface { private IStrategyResolver strategyResolver; public SomeClass(IStrategyResolver stratResolver) { this.strategyResolver = stratResolver; } public void Process(SomeDto dto) { IActionHandler actionHanlder = this.strategyResolver.Resolve<IActionHandler>(dto.SomeProperty); actionHanlder.Handle(dto); } }

Registro:

container.RegisterType<IActionHandler, ActionOne>("One"); container.RegisterType<IActionHandler, ActionTwo>("Two"); container.RegisterType<IStrategyResolver, StrategyResolver>(); container.RegisterType<ISomeInterface, SomeClass>();

Ahora, lo bueno de esto es que nunca más tendré que volver a tocar StrategyResolver cuando agregue nuevas estrategias en el futuro.

Es muy sencillo. Muy limpio y mantuve la dependencia en Unity a un mínimo estricto. La única vez que habría tocado el StrategyResolver es si decido cambiar la tecnología del contenedor, que es muy poco probable que suceda.

¡Espero que esto ayude!