Как использовать Шаблон Декоратора с Unity без явного указания каждого параметра в InjectionConstructor - PullRequest
34 голосов
/ 24 мая 2011

Эта полезная статья от David Haydn (EDIT: мошенническая ссылка удалена, возможно, это была эта статья ) показывает, как вы можете использовать класс InjectionConstructor, чтобы помочь вам создать цепочку с использованием шаблона декораторас единством.Однако, если элементы в вашей цепочке декораторов имеют другие параметры в своем конструкторе, InjectionConstructor должен явно объявить каждый из них (или Unity будет жаловаться, что не может найти правильный конструктор).Это означает, что вы не можете просто добавить новые параметры конструктора к элементам в цепочке декораторов, не обновив также свой код конфигурации Unity.

Вот несколько примеров кода, чтобы объяснить, что я имею в виду.Класс 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>());
    }

Ответы [ 6 ]

25 голосов
/ 24 мая 2011

Другой подход, благодаря предложению @ 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>());
12 голосов
/ 28 июля 2011

См. эту статью о реализации расширения контейнера декоратора.Это должно привести вас туда, где вы хотите быть, в отношении того, что вам не нужно изменять вашу конфигурацию, если ваши сигнатуры конструктора изменяются.

6 голосов
/ 11 сентября 2013

Другое решение включает в себя добавление параметров типа в вашу кодовую базу, чтобы помочь 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, эти параметры шаблона больше никому не будут нужны.
4 голосов
/ 21 апреля 2017

Я выбрал довольно грубый метод расширения для этого, который вел себя как ожидалось, когда я его запустил:

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>();
3 голосов
/ 01 мая 2017

Самый краткий ответ, который отлично работает упоминается в другом stackoverflow посте Марка СиманаЭто лаконично и не требует от меня использования именованных регистраций или предложения использовать расширения Unity.Рассмотрим интерфейс под названием ILogger с двумя реализациями, а именно Log4NetLogger и реализацией декоратора под названием DecoratorLogger.Вы можете зарегистрировать DecoratorLogger для интерфейса ILogger следующим образом:

container.RegisterType<ILogger, DecoratorLogger>(
    new InjectionConstructor(
        new ResolvedParameter<Log4NetLogger>()));
0 голосов
/ 24 мая 2011

Пока я ждал ответов на этот вопрос, я нашел довольно хакерский обходной путь.Я создал метод расширения в 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

...