Инъекция зависимостей и шаблон стратегии

Существует огромное количество дискуссий по этой теме, но, похоже, все упускают очевидный ответ. Я хотел бы помочь проверить это "очевидное" решение контейнера МОК. Различные разговоры предполагают выбор стратегий во время выполнения и использование контейнера 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
  • Минусы:
    • Обычно привязывает вызывающего к контейнеру IOC и, безусловно, требует, чтобы вызывающий знал кое-что о стратегии (например, имя "Альфа").
    • Каждая новая стратегия должна быть вручную добавлена в список привязок.
    • Этот подход не подходит для обработки нескольких стратегий в графе объектов. Короче говоря, это не соответствует требованиям.

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

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

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>();
  • Плюсы:
    • Контейнер МОК скрыт от потребителей
    • "Окружающий контекст" ближе к желаемому результату, но...
  • Минусы:
    • Конструкторы каждой стратегии могут иметь разные потребности. Но теперь ответственность за инжекцию конструктора была перенесена на абстрактную фабрику из контейнера. Другими словами, каждый раз, когда добавляется новая стратегия, может потребоваться изменить соответствующую абстрактную фабрику.
    • Интенсивное использование стратегий означает большое количество создания абстрактных фабрик. Было бы хорошо, если бы контейнер 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):

  • Плюсы:
    • Скрывает контейнер
    • Не нужно ни создавать бесконечные абстрактные фабрики, ни заставлять потребителей возиться с ними.
    • Нет необходимости вручную настраивать привязки контейнеров IOC при появлении новой стратегии.
    • Позволяет контейнеру сохранять средства управления жизненным циклом.
    • Поддерживает историю DI, что означает, что многопоточное приложение может создать весь объектный граф в потоке с правильными настройками локального хранилища потока.
  • Минусы:
    • Поскольку возвращаемый стратегией Type был недоступен при создании начальных привязок контейнера IOC, это означает, что при первом возврате этого типа может произойти незначительное снижение производительности. Другими словами, контейнер должен на месте отражать тип, чтобы узнать, какие конструкторы он имеет, чтобы он знал, как его внедрить. Все последующие вхождения этого типа должны быть быстрыми, потому что контейнер может кэшировать результаты, которые он обнаружил с первого раза. Вряд ли стоит упоминать об этом "мошенничестве", но я пытаюсь раскрыть его полностью.
    • ???

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

Ответ 1

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

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

Мои личные предпочтения - это подсказка о роли частичного типа.

Ответ 2

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

У меня довольно простой подход. Я просто создаю 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, я решил изменить технологию контейнера, которая вряд ли произойдет.

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

Ответ 3

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

предполагайте выбор стратегий и использование контейнера 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> 's):

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

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

Ответ 4

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

Я создаю factory, который по существу завершает экземпляр контейнера. См. Раздел в статье статьи под названием Factory на основе контейнера. По его словам, я делаю эту часть factory частью корня композиции.

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

Из ваших недостатков этих подходов:

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

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

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

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

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

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

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

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

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

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

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

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