Внедрение зависимостей и шаблон стратегии

По этой теме ведется огромное количество дискуссий, но все, кажется, упускают очевидный ответ. Я хотел бы помочь проверить это "очевидное" решение контейнера IOC. Различные разговоры предполагают выбор стратегий во время выполнения и использование контейнера IOC. Я продолжу с этими предположениями.

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

Сначала я кратко обрисую два обычно предлагаемых решения, а затем представлю «очевидную» альтернативу, в которой я хотел бы видеть поддержку контейнера IOC. Я буду использовать Unity в качестве синтаксиса примера, хотя мой вопрос не относится к Unity.

Именованные привязки

Этот подход требует, чтобы каждая новая стратегия имела привязку, добавляемую вручную:

Container.RegisterType<IDataAccess, DefaultAccessor>();
Container.RegisterType<IDataAccess, AlphaAccessor>("Alpha");
Container.RegisterType<IDataAccess, BetaAccessor>("Beta");

... а затем явно запрашивается правильная стратегия:

var strategy = Container.Resolve<IDataAccess>("Alpha");
  • Плюсы: простой и поддерживается всеми контейнерами IOC.
  • Cons:
    • Typically binds the caller to the IOC Container, and certainly requires the caller to know something about the strategy (such as the name "Alpha").
    • Каждую новую стратегию необходимо вручную добавлять в список привязок.
    • Этот подход не подходит для обработки нескольких стратегий в графе объектов. Короче говоря, это не соответствует требованиям.

Абстрактная фабрика

Чтобы проиллюстрировать этот подход, предположим, что следующие классы:

public class DataAccessFactory{
    public IDataAccess Create(string strategy){
        return //insert appropriate creation logic here.
    }
    public IDataAccess Create(){
        return //Choose strategy through ambient context, such as thread-local-storage.
    }
}
public class Consumer
{
    public Consumer(DataAccessFactory datafactory)
    {
        //variation #1. Not sufficient to meet requirements.
        var myDataStrategy = datafactory.Create("Alpha");
        //variation #2.  This is sufficient for requirements.
        var myDataStrategy = datafactory.Create();
    }
}

Затем контейнер IOC имеет следующую привязку:

Container.RegisterType<DataAccessFactory>();
  • Pros:
    • The IOC Container is hidden from consumers
    • «Окружающий контекст» ближе к желаемому результату, но ...
  • Cons:
    • The constructors of each strategy might have different needs. But now the responsibility of constructor injection has been transferred to the abstract factory from the container. In other words, every time a new strategy is added it may be necessary to modify the corresponding abstract factory.
    • Интенсивное использование стратегий означает создание большого количества абстрактных фабрик. Было бы неплохо, если бы контейнер IOC просто помогал немного.
    • Если это многопоточное приложение и «окружающий контекст» действительно обеспечивается локальным хранилищем потока, то к тому времени, когда объект использует внедренную абстрактную фабрику для создания необходимого типа, он может работать с другой поток, у которого больше нет доступа к необходимому значению локального хранилища потока.

Переключение типов / динамическое связывание

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

Синтаксис будет примерно таким:

Container.RegisterType(typeof(IDataAccess),
    new InjectionStrategy((c) =>
    {
        //Access ambient context (perhaps thread-local-storage) to determine
        //the type of the strategy...
        Type selectedStrategy = ...;
        return selectedStrategy;
    })
);

Обратите внимание, что InjectionStrategy не возвращает экземпляр IDataAccess. Вместо этого он возвращает описание типа, реализующего IDataAccess. Контейнер IOC затем будет выполнять обычное создание и «наращивание» этого типа, что может включать другие выбираемые стратегии.

Это отличается от стандартной привязки типа к делегату, которая в случае Unity кодируется следующим образом:

Container.RegisterType(typeof(IDataAccess),
    new InjectionFactory((c) =>
    {
        //Access ambient context (perhaps thread-local-storage) to determine
        //the type of the strategy...
        IDataAccess instanceOfSelectedStrategy = ...;
        return instanceOfSelectedStrategy;
    })
);

Вышеупомянутое на самом деле близко к удовлетворению общей потребности, но определенно не соответствует гипотетической Unity InjectionStrategy.

Сосредоточимся на первом примере (в котором использовалась гипотетическая Unity InjectionStrategy):

  • Pros:
    • Hides the container
    • Нет необходимости создавать бесконечные абстрактные фабрики или заставлять потребителей возиться с ними.
    • Нет необходимости вручную настраивать привязки контейнеров IOC, когда доступна новая стратегия.
    • Позволяет контейнеру сохранять средства управления жизненным циклом.
    • Поддерживает чистую историю DI, что означает, что многопоточное приложение может создать весь граф объектов в потоке с правильными настройками локального хранилища потока.
  • Cons:
    • Because the Type returned by the strategy was not available when the initial IOC container bindings were created, it means there may be a tiny performance hit the first time that type is returned. In other words, the container must on-the-spot reflect the type to discover what constructors it has, so that it knows how to inject it. All subsequent occurrences of that type should be fast, because the container can cache the results it found from the first time. This is hardly a "con" worth mentioning, but I'm trying for full-disclosure.
    • ???

Есть ли существующий контейнер IOC, который может вести себя подобным образом? У кого-нибудь есть собственный класс инъекций Unity, который достигает этого эффекта?


person Brent Arias    schedule 27.03.2014    source источник
comment
Чем ваш пример для Type Switching / Dynamic Binding реально отличается от Abstract Factory? В конечном итоге вам придется написать почти идентичный код для обоих. Просто один - это набор классов, а другой - набор Container.RegisterType вызовов для каждого типа.   -  person Caleb    schedule 28.03.2014


Ответы (5)


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

Нет причин полагаться на контейнер DI для этого, поскольку есть как минимум три способа сделать это независимо от контейнера:

Мое личное предпочтение - Подсказка роли частичного имени типа.

person Mark Seemann    schedule 28.03.2014
comment
@MarkSeemann Считается ли дурным тоном генерировать factories, которые не имеют прямого отношения к процессу начального связывания? Кроме того: рассматриваемая фабрика не внедряется в Customer конструктор? Это предполагает, что при запуске системы также не будет Customers. Запрещено ли использование new в проводной системе с IoC-контейнером? - person jungle_mole; 18.12.2015
comment
@jungle_mole Абстрактная фабрика также является вариантом: stackoverflow.com/a/1945023/126014 - person Mark Seemann; 18.12.2015

Я достиг этого требования во многих формах за последние пару лет. Во-первых, давайте выделим основные моменты, которые я вижу в вашем сообщении

предположить выбор стратегий во время выполнения и использование контейнера IOC ... добавить предположение, что необходимо выбирать не одну стратегию. Скорее, мне может потребоваться получить объект-граф, который имеет несколько стратегий ... [не должен] связывать вызывающий объект с контейнером IOC ... Каждую новую стратегию необходимо [не обязательно] вручную добавлять в список привязок. .. Было бы неплохо, если бы контейнер IOC просто помогал немного больше.

Я использовал Simple Injector в качестве контейнера в течение некоторого времени, и одним из драйверов для этого решения является то, что он имеет обширный поддержка дженериков. Именно с помощью этой функции мы реализуем ваши требования.

Я твердо уверен, что код должен говорить сам за себя, поэтому сразу приступлю ...

  • Я определил дополнительный класс ContainerResolvedClass<T>, чтобы продемонстрировать, что Simple Injector находит правильные реализации и успешно внедряет их в конструктор. Это единственная причина для класса ContainerResolvedClass<T>. (Этот класс предоставляет обработчики, которые вводятся в него для тестовых целей через result.Handlers.)

Этот первый тест требует, чтобы мы вернули одну реализацию для вымышленного класса Type1:

[Test]
public void CompositeHandlerForType1_Resolves_WithAlphaHandler()
{
    var container = this.ContainerFactory();

    var result = container.GetInstance<ContainerResolvedClass<Type1>>();
    var handlers = result.Handlers.Select(x => x.GetType());

    Assert.That(handlers.Count(), Is.EqualTo(1));
    Assert.That(handlers.Contains(typeof(AlphaHandler<Type1>)), Is.True);
}

Этот второй тест требует, чтобы мы вернули одну реализацию для вымышленного класса Type2:

[Test]
public void CompositeHandlerForType2_Resolves_WithAlphaHandler()
{
    var container = this.ContainerFactory();

    var result = container.GetInstance<ContainerResolvedClass<Type2>>();
    var handlers = result.Handlers.Select(x => x.GetType());

    Assert.That(handlers.Count(), Is.EqualTo(1));
    Assert.That(handlers.Contains(typeof(BetaHandler<Type2>)), Is.True);
}

Этот третий тест требует, чтобы мы вернули две реализации для вымышленного класса Type3:

[Test]
public void CompositeHandlerForType3_Resolves_WithAlphaAndBetaHandlers()
{
    var container = this.ContainerFactory();

    var result = container.GetInstance<ContainerResolvedClass<Type3>>();
    var handlers = result.Handlers.Select(x => x.GetType());

    Assert.That(handlers.Count(), Is.EqualTo(2));
    Assert.That(handlers.Contains(typeof(AlphaHandler<Type3>)), Is.True);
    Assert.That(handlers.Contains(typeof(BetaHandler<Type3>)), Is.True);
}

Кажется, что эти тесты соответствуют вашим требованиям, и, что лучше всего, в решении не повреждены контейнеры.


Уловка состоит в том, чтобы использовать комбинацию объектов параметров и интерфейсов маркеров. Объекты параметров содержат данные для поведения (т.е. IHandler), а интерфейсы маркеров определяют, какое поведение воздействует на какие объекты параметров.

Вот интерфейсы маркеров и объекты параметров - вы заметите, что Type3 отмечен обоими интерфейсами маркеров:

private interface IAlpha { }
private interface IBeta { }

private class Type1 : IAlpha { }
private class Type2 : IBeta { }
private class Type3 : IAlpha, IBeta { }

Вот поведение (IHandler<T>):

private interface IHandler<T> { }

private class AlphaHandler<TAlpha> : IHandler<TAlpha> where TAlpha : IAlpha { }
private class BetaHandler<TBeta> : IHandler<TBeta> where TBeta : IBeta { }

Это единственный метод, который найдет все реализации открытого дженерика:

public IEnumerable<Type> GetLoadedOpenGenericImplementations(Type type)
{
    var types =
        from assembly in AppDomain.CurrentDomain.GetAssemblies()
        from t in assembly.GetTypes()
        where !t.IsAbstract
        from i in t.GetInterfaces()
        where i.IsGenericType
        where i.GetGenericTypeDefinition() == type
        select t;

    return types;
}

И это код, который настраивает контейнер для наших тестов:

private Container ContainerFactory()
{
    var container = new Container();

    var types = this.GetLoadedOpenGenericImplementations(typeof(IHandler<>));

    container.RegisterAllOpenGeneric(typeof(IHandler<>), types);

    container.RegisterOpenGeneric(
        typeof(ContainerResolvedClass<>),
        typeof(ContainerResolvedClass<>));

    return container;
}

И, наконец, тестовый класс ContainerResolvedClass<>

private class ContainerResolvedClass<T>
{
    public readonly IEnumerable<IHandler<T>> Handlers;

    public ContainerResolvedClass(IEnumerable<IHandler<T>> handlers)
    {
        this.Handlers = handlers;
    }
}

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

person qujck    schedule 28.03.2014
comment
Фантастическая работа, которую мы очень ценим. Однако, как есть, две проблемы не позволяют мне ответить на этот вопрос. Во-первых, что, если я добавлю второй параметр конструктора в ContainerResolvedClass, и этот второй параметр представляет стратегию другого типа? Теперь я должен изменить определение ContainerResolvedClass. Во-вторых, чтобы разрешить конкретную стратегию (-ы), я все еще эффективно предоставляю (общий) параметр «T» для Container.GetInstance (ContainerResolvedClass ‹T›). Более того, в этом подходе есть склонность к поиску сервисов. Ищу чистый DI. Я обновлю параметры своего вопроса. - person Brent Arias; 29.03.2014
comment
@BrentArias Container.GetInstance - это тестовый код. Вам нужно только указать контейнер в корне композиции. Я рассмотрел пример двух стратегий с помощью теста Type3, в котором вы получаете два поведения для одного типа. Я специально не закодировал это для соответствия шаблону стратегии, потому что он в равной степени применим к шаблонам наблюдателя и посетителя. Я написал это как общий пример кода, который предоставит вам одну или несколько реализаций, основанных исключительно на коде / конфигурации, без каких-либо ссылок на контейнер, кроме корня композиции. - person qujck; 29.03.2014
comment
Этот код не может ничего возвращать, так же как и переключение типов, и способ смягчить его в любом случае - использовать одноэлементный NullHandler, чтобы вернуться к нему. - person qujck; 29.03.2014

Это запоздалый ответ, но, возможно, он поможет другим.

У меня довольно простой подход. Я просто создаю StrategyResolver, чтобы не зависеть напрямую от Unity.

public class StrategyResolver : IStrategyResolver
{
    private IUnityContainer container;

    public StrategyResolver(IUnityContainer unityContainer)
    {
        this.container = unityContainer;
    }

    public T Resolve<T>(string namedStrategy)
    {
        return this.container.Resolve<T>(namedStrategy);
    }
}

Использование:

public class SomeClass: ISomeInterface
{
    private IStrategyResolver strategyResolver;

    public SomeClass(IStrategyResolver stratResolver)
    {
        this.strategyResolver = stratResolver;
    }

    public void Process(SomeDto dto)
    {
        IActionHandler actionHanlder = this.strategyResolver.Resolve<IActionHandler>(dto.SomeProperty);
        actionHanlder.Handle(dto);
    }
}

Регистрация:

container.RegisterType<IActionHandler, ActionOne>("One");
container.RegisterType<IActionHandler, ActionTwo>("Two");
container.RegisterType<IStrategyResolver, StrategyResolver>();
container.RegisterType<ISomeInterface, SomeClass>();

Самое приятное в этом то, что мне никогда больше не придется прикасаться к StrategyResolver, когда я добавляю новые стратегии в будущем.

Все очень просто. Очень чисто, и я свел зависимость от Unity до минимума. Единственный раз, когда я прикоснусь к StrategyResolver, - это если я решу изменить технологию контейнера, что вряд ли произойдет.

Надеюсь это поможет!

person TchiYuan    schedule 17.06.2016
comment
В вашем решении используется локатор служб, который считается анти-шаблоном из-за аргументов, связанных с пользовательским интерфейсом, надежностью и инкапсуляцией. - person alexb; 06.05.2019

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

Что я делаю, так это создаю фабрику, которая по существу обертывает экземпляр контейнера. См. Раздел статьи Марка под названием Фабрика на основе контейнеров . Как он предлагает, я делаю эту фабрику частью корня композиции.

Чтобы сделать мой код немного чище и менее основанным на «магической строке», я использую перечисление для обозначения различных возможных стратегий и использую метод .ToString () для регистрации и разрешения.

Из ваших минусов этих подходов:

Обычно привязывает вызывающего к контейнеру IOC

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

. . . и, безусловно, требует, чтобы вызывающий знал что-то о стратегии (например, название «Альфа»).

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

В какой-то момент необходимо написать код, подтверждающий соответствие между структурой, обеспечивающей реализацию (контейнер, поставщик, фабрика и т. Д.), И кодом, который этого требует. Я не думаю, что вы можете обойти это, если не хотите использовать что-то чисто условное.

У конструкторов каждой стратегии могут быть разные потребности. Но теперь ответственность за внедрение конструктора перенесена на абстрактную фабрику из контейнера. Другими словами, каждый раз, когда добавляется новая стратегия, может потребоваться изменить соответствующую абстрактную фабрику.

Такой подход полностью решает эту проблему.

Интенсивное использование стратегий означает создание большого количества абстрактных фабрик. [...]

Да, вам понадобится одна абстрактная фабрика для каждого набора стратегий.

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

Это больше не будет проблемой, поскольку TLC не будет использоваться.

Я не думаю, что есть идеальное решение, но этот подход хорошо сработал для меня.

person Phil Sandler    schedule 28.03.2014

Я бы реализовал что-то вроде этого.

public interface IAbstractFactory
{
    IFiledAppSettingsFactory this[Provider provider] { get; }
}

public Enum : int 
{ 
   One =1, Two =2, Three =3
}


internal class AbstractFactory : IAbstractFactory
{
    public AbstractFactory(/** dependencies **/)
    {
    }

    private readonly IReadOnlyDictionary<Provider, IFactory> services
       = new Dictionary<Provider, IFactory>
    {
       { Provider.One , new Factory1(/** dependencies comming from AbstractFactory **/) },
       { Provider.Two , new Factory2(/** no dependencies **/) },
       { Provider.Three, new Factory3(/** maybe more dependencies comming from AbstractFactory **/) },
    };

    IFactory IAbstractFactory.this[Provider provider] => this.services[provider];
}

internal sealed class Factory1: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

internal sealed class Factory2: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

internal sealed class Factory3: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

public static void AddAppSettings(this IServiceCollection serviceDescriptors)
{
    serviceDescriptors.AddSingleton<IAbstractFactory, AbstractFactory>();
}


public class Consumer
{
    private readonly IFactory realFactory;
    public Consumer(IIAbstractFactory factory) 
    {
          realFactory = factory[Provider.One]
    }
}
person boris    schedule 14.11.2020