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