如何在不显式指定InjectionConstructor中的每个参数的情况下,将Decorator模式与Unity一起使

本文关键字:情况下 Decorator 参数 模式 一起 Unity InjectionConstructor | 更新日期: 2023-09-27 17:57:26

David Haydon的这篇有用的文章(编辑:删除了骗局链接,可能是这篇文章)展示了如何使用InjectionConstructor类来帮助您使用Unity的装饰器模式建立链。但是,如果装饰器链中的项在其构造函数中有其他参数,则InjectionConstructor必须显式声明其中的每一个(否则Unity将抱怨找不到正确的构造函数)。这意味着,在不更新Unity配置代码的情况下,您不能简单地向decorator链中的项添加新的构造函数参数。

下面是一些示例代码来解释我的意思。ProductRepository类首先由CachingProductRepository封装,然后由LoggingProductRepostiory封装。CachingProductRepository和LoggingProductRepository除了在其构造函数中使用IProductRepository外,还需要容器中的其他接口。

    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);
        }
    }

这是一个(通过的)单元测试。查看剩余配置的注释,我想删除对的需求

    [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>());
    }

如何在不显式指定InjectionConstructor中的每个参数的情况下,将Decorator模式与Unity一起使

根据@DarkSquirrel42的建议,另一种方法是使用InjectionFactory。缺点是,每次向链中的某个对象添加新的构造函数参数时,代码仍然需要更新。其优点是更容易理解代码,并且只需在容器中进行一次注册。

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>());

请参阅本文中关于实现装饰器容器扩展的内容。如果构造函数签名发生更改,就不需要修改配置,这将使您达到您想要的位置。

另一个解决方案涉及将类型参数添加到代码库中,以帮助Unity解析修饰的类型。幸运的是,Unity完全能够自行解析类型参数及其依赖项,因此在定义装饰器链时,我们不必关心构造函数参数。

注册情况如下:

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

下面是一个基本的示例实现。注意模板化的装饰器Logged<TService>Profiled<TService>。下面是我注意到的一些缺点。

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();
    }
}

缺点

  • uC.RegisterType<IService, Logged<IService>>();这样的错误注册将导致堆栈溢出应用程序的无限递归。这可能是插件体系结构中的一个漏洞
  • 它在某种程度上丑化了你的代码库。如果你放弃了Unity,转而使用不同的DI框架,那么这些模板参数对任何人来说都没有意义了

我为此开发了一个相当粗糙的扩展方法,当我运行它时,它的行为正如预期的那样:

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;
    }
}

在您的设置中:

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>();

Mark Seeman在另一篇stackoverflow帖子中提到了最简洁的答案,效果很好。它是简洁,不要求我使用命名注册,也不建议我使用Unity扩展。考虑一个名为ILogger的接口,该接口有两个实现,即Log4NetLogger和一个称为DecoratorLogger的装饰器实现。您可以根据ILogger接口注册DecoratorLogger,如下所示:

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

在等待答案时,我想出了一个相当巧妙的解决方法。我在IUnityContainer上创建了一个扩展方法,允许我使用反射注册装饰器链来创建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;
        }
    }
}

这使我可以用更可读的语法配置我的容器:

[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>());
}

我仍然很想知道Unity 是否有更优雅的解决方案

我知道这篇文章有点过时,但事实上,最新版本没有完全功能的Unity装饰器实现(有很多突破性的更改,请参阅Unity wiki)。

我接受了@garryp答案(在我看来,这是这里唯一正确的答案),并根据最新的Unity容器API更改进行了修改:

public static IContainerRegistry RegisterDecorator<TInterface, TDecorator>(this IContainerRegistry container, ITypeLifetimeManager lifetimeManager, Type[] additionalInterfaces, params InjectionMember[] injectionMembers)
    where TDecorator : class, TInterface
{    
    var unityContainer = container.GetContainer();
    var existingRegistration = unityContainer.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;
    var uniqueId = Guid.NewGuid().ToString();
    // 1. Create a wrapper. This is the actual resolution that will be used
    if (lifetimeManager != null)
    {
        unityContainer.RegisterType<TDecorator>(uniqueId, lifetimeManager, injectionMembers);
    }
    else
    {
        unityContainer.RegisterType<TDecorator>(uniqueId, injectionMembers);
    }
    unityContainer.RegisterType<TInterface, TDecorator>();
    if (additionalInterfaces != null)
    {
        foreach (var additionalInterface in additionalInterfaces)
        {
            unityContainer.RegisterType(additionalInterface, typeof(TDecorator));
        }
    }
    unityContainer.RegisterFactory<TDecorator>(DecoratorFactory);
    return container;
    object DecoratorFactory(IUnityContainer c)
    {
        // 3. We get the decorated class instance TBase
        var baseObj = c.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));
    }
}

区别在于:

  • 正在使用IContainerRegistry类型而不是IUnityContainer——这是因为我在Unity容器上使用PRISM包装器
  • 添加了additionalInterfaces可选参数,以便能够注册也实现其他接口的装饰器
  • 修改逻辑,使其适合当前的Unity API实现