如何在Unity中使用装饰器模式而不需要显式地指定InjectionConstructor中的每个参数?

39
这篇来自David Haydn的有用文章(编辑:删除骗子链接,可能是这篇文章)展示了如何使用InjectionConstructor类在Unity中使用装饰者模式设置链。但是,如果您的装饰器链中的项在其构造函数中具有其他参数,则InjectionConstructor必须明确声明每一个参数(否则Unity会抱怨找不到正确的构造函数)。这意味着在装饰器链中仅添加新的构造函数参数并不足以,还需要更新Unity配置代码。

以下是一些示例代码,以解释我的意思。 ProductRepository类首先被CachingProductRepository包装,然后再被LoggingProductRepostiory包装。除了在其构造函数中接受IProductRepository之外,CachingProductRepository和LoggingProductRepository还需要容器中的其他接口。
    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>());
    }

2
我以前从未使用过Unity,也许这有些偏离主题,但是你不能使用InjectionFactory代替InjectionConstructor吗? - DarkSquirrel42
@DarkSquirrel42 一个有趣的建议。动作(Action)可能需要调用具体的构造函数,这意味着Unity配置代码仍然需要在构造函数更改时被调用。虽然这至少会导致编译错误,提醒您需要进行更改。 - Mark Heath
@DarkSquirrel42,经过再次考虑,如果您只使用一个InjectionFactory,其Func构建整个链,并使用容器来满足其他依赖项,尽管您仍然无法避免构造函数参数的更改,但至少比我的原始代码要清晰得多,并且还只需要在容器中注册一次。 - Mark Heath
1
链接到文章已经失效。 - Josh Noe
那篇“有用的文章”现在成了一个骗局网站,你可能需要修复一下。 - Anthony Raimondo
指出了链接失效的人都没有编辑权限吗?那就快去修复它。 - BenCr
7个回答

29

另一种方法是使用InjectionFactory,这得益于@DarkSquirrel42的建议。缺点是每次在链中添加新的构造函数参数时仍需要更新代码。优点是代码更容易理解,并且只需在容器中进行一次注册。

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

8
这样做的好处在于构造函数的更改将会在编译时发现而不是运行时发生错误。 - Peter Ruderman

13

请参考这篇文章,介绍了如何实现一个装饰器容器扩展。通过这种方式,即使构造函数签名发生变化,也无需修改配置即可达到所需效果。


这是我最喜欢的解决方案! - Jan

6
另一种解决方案涉及向代码库添加类型参数,以帮助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框架,那些模板参数将对任何人都没有意义。

5
我为此编写了一个相当简陋的扩展方法,当我运行它时,它的表现符合预期:
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>();

你能解释一下这个如何处理递归吗?我无法理解 container.Registrations.First(r => r.RegisteredType == typeof(TInterface)).Mapped 到类型... - Johnny
你引用的那一行代码将查看现有的注册信息,获取要装饰的具体类型(即 existing),并记录下该具体类型;然后会注册装饰器,替换掉 existing。在解析时,装饰器将使用该具体类型(即 existing)并将其注入到装饰器中,但你的接口的所有其他用法都将使用该装饰器。这样,如果需要的话,就可以多次进行装饰。如果使用了命名注册,则可能需要进行调整。 - garryp
我理解了你说的,但是不明白为什么你总是从注册列表的第一个开始检索。如果有多个已注册的类型具有相同的接口,该怎么处理呢? - Johnny
你为同一个接口注册两种类型的唯一方法是使用命名注册来区分它们(因此有了First() - 它假设只会得到一个,也许Single更好,我不知道...),否则就会出现冲突(在给定情况下Unity如何知道使用哪个?)。如果您正在使用命名注册,则需要调整此代码。 - garryp
这很不错!我给个明确的+1,但我有点想知道为什么你说它“粗糙”?代码看起来很好,而且正好做了它应该做的事情。唯一我会改变/添加的是,在查找“existing”类型时进行检查——如果它还没有被注册,则抛出一个带有适当消息的异常……并在查找类型时确保它没有被注册为名称(尽管这是有争议的)。 - MBender
我同意,我已经修改了帖子以检查现有的注册。由于存在一个错误,即每次此代码始终装饰第一个匹配的注册表,因此我稍微更改了此代码,因此“existing”现在是具有匹配接口的最后一个注册表,而不是第一个注册表。 - garryp

3

另一篇由Mark Seeman在stackoverflow post中提到的最简洁且非常有效的答案。它很简洁,不需要我使用命名注册或建议我使用Unity扩展。考虑一个名为ILogger的接口,其中包含两个实现,即Log4NetLogger和名为DecoratorLogger的装饰器实现。您可以将DecoratorLogger注册到ILogger接口中,如下所示:

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

0

在等待答案的过程中,我想出了一个相当hacky的解决方法。我创建了一个扩展方法,在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中是否有更优雅的解决方案


0

我知道这篇文章有点过时了,但事实上最新版本没有完全功能的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实现

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接