2011-05-24 11 views
25

Este útil artículo de David Haydn (EDITAR: se eliminó el enlace fraudulento, pudo haber sido this article) muestra cómo puede usar la clase InjectionConstructor para ayudarlo configura una cadena usando el patrón de decorador con Unity. Sin embargo, si los elementos en su cadena decoradora tienen otros parámetros en su constructor, el InjectionConstructor debe declarar explícitamente cada uno de ellos (o Unity se quejará de que no puede encontrar el constructor correcto). Esto significa que no puede simplemente agregar nuevos parámetros de constructor a los elementos en la cadena de decoradores sin actualizar también su código de configuración de Unity.Cómo uso el Patrón Decorador con Unity sin especificar explícitamente cada parámetro en InjectionConstructor

Aquí hay un código de ejemplo para explicar a qué me refiero. La clase ProductRepository está envuelta primero por CachingProductRepository y luego por LoggingProductRepostiory. Tanto CachingProductRepository como LoggingProductRepository, además de tomar un IProductRepository en su constructor, también necesitan otras interfaces desde el contenedor.

public class Product 
    { 
     public int Id; 
     public string Name; 
    } 

    public interface IDatabaseConnection { } 

    public interface ICacheProvider 
    { 
     object GetFromCache(string key); 
     void AddToCache(string key, object value); 
    } 

    public interface ILogger 
    { 
     void Log(string message, params object[] args); 
    } 


    public interface IProductRepository 
    { 
     Product GetById(int id);  
    } 

    class ProductRepository : IProductRepository 
    { 
     public ProductRepository(IDatabaseConnection db) 
     { 
     } 

     public Product GetById(int id) 
     { 
      return new Product() { Id = id, Name = "Foo " + id.ToString() }; 
     } 
    } 

    class CachingProductRepository : IProductRepository 
    { 
     IProductRepository repository; 
     ICacheProvider cacheProvider; 
     public CachingProductRepository(IProductRepository repository, ICacheProvider cp) 
     { 
      this.repository = repository; 
      this.cacheProvider = cp; 
     } 

     public Product GetById(int id) 
     {  
      string key = "Product " + id.ToString(); 
      Product p = (Product)cacheProvider.GetFromCache(key); 
      if (p == null) 
      { 
       p = repository.GetById(id); 
       cacheProvider.AddToCache(key, p); 
      } 
      return p; 
     } 
    } 

    class LoggingProductRepository : IProductRepository 
    { 
     private IProductRepository repository; 
     private ILogger logger; 

     public LoggingProductRepository(IProductRepository repository, ILogger logger) 
     { 
      this.repository = repository; 
      this.logger = logger; 
     } 

     public Product GetById(int id) 
     { 
      logger.Log("Requesting product {0}", id); 
      return repository.GetById(id); 
     } 
    } 

Aquí hay una prueba de unidad (pasajera). Véanse los comentarios de los bits de configuración del excedente Quiero eliminar la necesidad de:

[Test] 
    public void ResolveWithDecorators() 
    { 
     UnityContainer c = new UnityContainer();    
     c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object); 
     c.RegisterInstance<ILogger>(new Mock<ILogger>().Object); 
     c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object); 

     c.RegisterType<IProductRepository, ProductRepository>("ProductRepository"); 

     // don't want to have to update this line every time the CachingProductRepository constructor gets another parameter 
     var dependOnProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("ProductRepository"), new ResolvedParameter<ICacheProvider>()); 
     c.RegisterType<IProductRepository, CachingProductRepository>("CachingProductRepository", dependOnProductRepository); 

     // don't want to have to update this line every time the LoggingProductRepository constructor changes 
     var dependOnCachingProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("CachingProductRepository"), new ResolvedParameter<ILogger>()); 
     c.RegisterType<IProductRepository, LoggingProductRepository>(dependOnCachingProductRepository); 
     Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>()); 
    } 
+2

así, nunca he usado la unidad antes y tal vez esto está muy lejos, pero no se puede utilizar en lugar de injectionfactory injectionconstructor? – DarkSquirrel42

+0

@ DarkSquirrel42 una sugerencia interesante. La Acción, presumiblemente, tendría que llamar al constructor concreto, lo que significa que el código de configuración de Unity debería llamarse siempre que el construtor cambiara. Sin embargo, al menos generaría un error de compilación, lo que alertaría sobre la necesidad de un cambio. –

+0

@ DarkSquirrel42, pensándolo bien, si solo usa una sola InjectionFactory cuya Func construye toda la cadena, usar el contenedor para cumplir otras dependencias, aunque todavía no esté aislado de los cambios en los parámetros del constructor, es al menos mucho menos confuso que mi original código, y requiere solo un registro en el contenedor también. –

Respuesta

23

Otro enfoque, gracias a una sugerencia de @ DarkSquirrel42, es utilizar un InjectionFactory. La desventaja es que el código aún necesita actualizarse cada vez que se agrega un nuevo parámetro constructor a algo en la cadena. Las ventajas son mucho más fáciles de entender el código y solo un registro único en el contenedor.

Func<IUnityContainer,object> createChain = container => 
    new LoggingProductRepository(
     new CachingProductRepository(
      container.Resolve<ProductRepository>(), 
      container.Resolve<ICacheProvider>()), 
     container.Resolve<ILogger>()); 

c.RegisterType<IProductRepository>(new InjectionFactory(createChain)); 
Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>()); 
+6

Esto también tiene la ventaja de que los cambios en los constructores generarán tiempo de compilación en lugar de errores de tiempo de ejecución. –

0

Mientras estaba esperando respuestas sobre este, se me ocurrió una solución en lugar de hacky. He creado un método de extensión en IUnityContainer que me permite registrar una cadena de decoradores utilizando la reflexión para crear los parámetros InjectionConstructor:

static class DecoratorUnityExtensions 
{ 
    public static void RegisterDecoratorChain<T>(this IUnityContainer container, Type[] decoratorChain) 
    { 
     Type parent = null; 
     string parentName = null; 
     foreach (Type t in decoratorChain) 
     { 
      string namedInstance = Guid.NewGuid().ToString(); 
      if (parent == null) 
      { 
       // top level, just do an ordinary register type      
       container.RegisterType(typeof(T), t, namedInstance); 
      } 
      else 
      { 
       // could be cleverer here. Just take first constructor 
       var constructor = t.GetConstructors()[0]; 
       var resolvedParameters = new List<ResolvedParameter>(); 
       foreach (var constructorParam in constructor.GetParameters()) 
       { 
        if (constructorParam.ParameterType == typeof(T)) 
        { 
         resolvedParameters.Add(new ResolvedParameter<T>(parentName)); 
        } 
        else 
        { 
         resolvedParameters.Add(new ResolvedParameter(constructorParam.ParameterType)); 
        } 
       } 
       if (t == decoratorChain.Last()) 
       { 
        // not a named instance 
        container.RegisterType(typeof(T), t, new InjectionConstructor(resolvedParameters.ToArray())); 
       } 
       else 
       { 
        container.RegisterType(typeof(T), t, namedInstance, new InjectionConstructor(resolvedParameters.ToArray())); 
       } 
      } 
      parent = t; 
      parentName = namedInstance; 
     } 
    } 
} 

Esto me permite configurar mi recipiente con una sintaxis mucho más legible:

[Test] 
public void ResolveWithDecorators2() 
{ 
    UnityContainer c = new UnityContainer(); 
    c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object); 
    c.RegisterInstance<ILogger>(new Mock<ILogger>().Object); 
    c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object); 

    c.RegisterDecoratorChain<IProductRepository>(new Type[] { typeof(ProductRepository), typeof(CachingProductRepository), typeof(LoggingProductRepository) }); 

    Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>()); 

} 

todavía estaría interesado en saber si hay una solución más elegante a este con Unity

11

Consulte this article para implementar una extensión de contenedor decorador. Esto debería llevarte a donde quieres estar con respecto a no necesitar modificar tu configuración si tus firmas de constructor cambian.

+0

¡Esta es mi solución favorita! – Jan

5

Otra solución consiste en agregar parámetros de tipo a su código base para ayudar a Unity a resolver sus tipos decorados. Afortunadamente, Unity es perfectamente capaz de resolver los parámetros de tipo y sus dependencias por sí mismo, por lo que no tenemos que preocuparnos por los parámetros del constructor al definir la cadena decoradora.

El registro se vería de la siguiente manera:

unityContainer.RegisterType<IService, Logged<Profiled<Service>>>(); 

Aquí es una implementación básica ejemplo. Tenga en cuenta los decoradores con plantilla Logged<TService> y Profiled<TService>. Observe a continuación algunos inconvenientes que he notado hasta ahora.

public interface IService { void Do(); } 

public class Service : IService { public void Do() { } } 

public class Logged<TService> : IService where TService : IService 
{ 
    private TService decoratee; 
    private ILogger logger; 

    public Logged(ILogger logger, TService decoratee) { 
     this.decoratee = decoratee; 
     this.logger = logger; 
    } 

    public void Do() { 
     logger.Debug("Do()"); 
     decoratee.Do(); 
    } 
} 

public class Profiled<TService> : IService where TService : IService 
{ 
    private TService decoratee; 
    private IProfiler profiler; 

    public Profiled(IProfiler profiler, TService decoratee) { 
     this.decoratee = decoratee; 
     this.profiler = profiler; 
    } 

    public void Do() { 
     profiler.Start(); 
     decoratee.Do(); 
     profiler.Stop(); 
    } 
} 

Inconvenientes

  • Un registro defectuoso como uC.RegisterType<IService, Logged<IService>>(); dará lugar a un bucle infinito que apilar-desborda su aplicación. Esto puede ser una vulnerabilidad en una arquitectura de complemento.
  • En cierta medida, daña tu código base. Si alguna vez renuncia a Unity y cambia a un marco de DI diferente, los parámetros de la plantilla ya no tendrán sentido para nadie.
2

Eliminé un método de extensión bastante crudo para esto, que se comportó como se esperaba cuando me encontré con él:

public static class UnityExtensions 
{ 
    public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, params InjectionMember[] injectionMembers) 
     where TDecorator : class, TInterface 
    { 
     return Decorate<TInterface, TDecorator>(container, null, injectionMembers); 
    } 

    public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers) 
     where TDecorator : class, TInterface 
    { 
     string uniqueId = Guid.NewGuid().ToString(); 
     var existingRegistration = container.Registrations.LastOrDefault(r => r.RegisteredType == typeof(TInterface)); 
     if(existingRegistration == null) 
     { 
      throw new ArgumentException("No existing registration found for the type " + typeof(TInterface)); 
     } 
     var existing = existingRegistration.MappedToType; 

     //1. Create a wrapper. This is the actual resolution that will be used 
     if (lifetimeManager != null) 
     { 
      container.RegisterType<TInterface, TDecorator>(uniqueId, lifetimeManager, injectionMembers); 
     } 
     else 
     { 
      container.RegisterType<TInterface, TDecorator>(uniqueId, injectionMembers); 
     } 

     //2. Unity comes here to resolve TInterface 
     container.RegisterType<TInterface, TDecorator>(new InjectionFactory((c, t, sName) => 
     { 
      //3. We get the decorated class instance TBase 
      var baseObj = container.Resolve(existing); 

      //4. We reference the wrapper TDecorator injecting TBase as TInterface to prevent stack overflow 
      return c.Resolve<TDecorator>(uniqueId, new DependencyOverride<TInterface>(baseObj)); 
     })); 

     return container; 
    } 
} 

Y en su configuración:

container.RegisterType<IProductRepository, ProductRepository>(); 

// Wrap ProductRepository with CachingProductRepository, 
// injecting ProductRepository into CachingProductRepository for 
// IProductRepository 
container.Decorate<IProductRepository, CachingProductRepository>(); 

// Wrap CachingProductRepository with LoggingProductRepository, 
// injecting CachingProductRepository into LoggingProductRepository for 
// IProductRepository 
container.Decorate<IProductRepository, LoggingProductRepository>(); 
+0

¿Podría explicarnos cómo se relaciona esto con la recursión? No puedo entender container.Registrations.First (r => r.RegisteredType == typeof (TInterface)). Asignado para escribir ... – Johnny

+0

La línea que hace referencia mirará los registros existentes, obtendrá el registro existente para la interfaz (el concreto para decorar - 'existente') y mantener una nota de ese tipo concreto; el decorador se registra luego, reemplazando 'existing'. Cuando se trata de resolución, el decorador usará ese tipo concreto ('existente') e inyectarlo en el decorador, pero todos los otros usos de su interfaz usarán el decorador. De esta manera es posible decorar muchas veces si es necesario. Puede necesitar una adaptación si usa registros con nombre. – garryp

+0

Entiendo eso pero no entiendo por qué siempre recuperas primero de la lista de registros. ¿Qué ocurre si tiene más de un tipo registrado con la misma interfaz? – Johnny

2

El más sucinta respuesta que funciona bien se menciona en otro stackoverflow post por Mark Seeman. Es conciso, y no requiere que use registros con nombre ni sugiero que use extensiones de Unity. Considere una interfaz llamada ILogger con dos implementaciones, concretamente Log4NetLogger y una implementación de decorador llamada DecoratorLogger. Puede registrar el DecoratorLogger contra la interfaz ILogger de la siguiente manera:

container.RegisterType<ILogger, DecoratorLogger>(
    new InjectionConstructor(
     new ResolvedParameter<Log4NetLogger>())); 
Cuestiones relacionadas