Подтвердить что ты не робот

Как использовать шаблон Decorator с Unity без явного указания каждого параметра в InjectionConstructor

Эта полезная статья Дэвида Гайдна (EDIT: удаленная ссылка удалена, возможно, она была в этой статье) показывает, как вы можете использовать InjectionConstructor, чтобы помочь вам создать цепочку, используя шаблон декоратора с Unity. Однако, если элементы в вашей цепочке декораторов имеют другие параметры в своем конструкторе, 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);
        }
    }

Здесь (прохождение) unit test. См. Комментарии к битам избыточной конфигурации. Я хочу удалить необходимость:

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

Ответ 1

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

Ответ 2

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

Ответ 3

Другое решение включает добавление параметров типа к вашей базе кода, чтобы помочь 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

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

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

Ответ 5

Самый сжатый ответ, который отлично работает, упоминается в другом сообщении fooobar.com/questions/57600/... от Mark Seeman. Он лаконичен и не требует от меня использования названных регистраций или предполагает, что я использую расширения Unity. Рассмотрим интерфейс ILogger с двумя реализациями, а именно Log4NetLogger и реализацией декоратора под названием DecoratorLogger. Вы можете зарегистрировать DecoratorLogger для интерфейса ILogger следующим образом:

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

Ответ 6

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