По этой теме ведется огромное количество дискуссий, но все, кажется, упускают очевидный ответ. Я хотел бы помочь проверить это "очевидное" решение контейнера 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. - ???
- Because the
Есть ли существующий контейнер IOC, который может вести себя подобным образом? У кого-нибудь есть собственный класс инъекций Unity, который достигает этого эффекта?
Type Switching / Dynamic Binding
реально отличается отAbstract Factory
? В конечном итоге вам придется написать почти идентичный код для обоих. Просто один - это набор классов, а другой - наборContainer.RegisterType
вызовов для каждого типа. - person Caleb   schedule 28.03.2014