Для целей этого обсуждения конструктор объекта может принимать два вида параметров: зависимость от состояния или зависимость от службы. Обеспечить служебную зависимость с контейнером IOC легко: DI вступает во владение. Но, напротив, зависимости от состояния обычно известны только клиенту. То есть объект-запросчик.
Оказывается, что наличие у клиента параметров состояния через контейнер IOC довольно болезненно. Я покажу несколько разных способов сделать это, у всех из которых есть большие проблемы, и спрошу сообщество, есть ли другой вариант, который я пропускаю. Давайте начнем:
Прежде чем добавить контейнер IOC в код своего проекта, я начал с такого класса:
class Foobar {
//parameters are state dependencies, not service dependencies
public Foobar(string alpha, int omega){...};
//...other stuff
}
Я решил добавить зависимость службы Logger в класс Foobar, которую, возможно, я предоставлю через DI:
class Foobar {
public Foobar(string alpha, int omega, ILogger log){...};
//...other stuff
}
Но затем мне также сказали, что мне нужно сделать сам класс Foobar "заменяемым". То есть мне нужно найти сервисный экземпляр Foobar. Я добавляю новый интерфейс в смесь:
class Foobar : IFoobar {
public Foobar(string alpha, int omega, ILogger log){...};
//...other stuff
}
Когда я выполняю вызов службы локатора, она определяет мою зависимость от службы ILogger. К сожалению, этого не скажешь о государственных зависимостях Альфа и Омега. Некоторые контейнеры предлагают синтаксис для решения этой проблемы:
//Unity 2.0 pseudo-ish code:
myContainer.Resolve<IFoobar>(
new parameterOverride[] { {"alpha", "one"}, {"omega",2} }
);
Мне нравится эта функция, но мне не нравится, что она не типизирована и не дает разработчику понять, какие параметры должны быть переданы (через intellisense и т. Д.). Поэтому я смотрю на другое решение:
//This is a "boiler plate" heavy approach!
class Foobar : IFoobar {
public Foobar (string alpha, int omega){...};
//...stuff
}
class FoobarFactory : IFoobarFactory {
public IFoobar IFoobarFactory.Create(string alpha, int omega){
return new Foobar(alpha, omega);
}
}
//fetch it...
myContainer.Resolve<IFoobarFactory>().Create("one", 2);
Вышесказанное решает проблему безопасности типов и intellisense, но оно (1) заставило класс Foobar извлекать ILogger через сервисный локатор, а не DI, и (2) он требует, чтобы я сделал кучу котла (XXXFactory , IXXXFactory) для всех разновидностей реализаций Foobar, которые я мог бы использовать. Если я решу пойти с подходом чистого сервиса, это может не быть проблемой. Но я все еще не выношу всю плиту, необходимую для этой работы.
Итак, я попробую другой вариант поддержки, предоставляемой контейнером:
//overall, this is a pseudo-memento pattern.
class Foobar : IFoobar {
public Foobar (FoobarParams parms){
this.alpha = parms.alpha;
this.omega = parms.omega;
};
//...stuff
}
class FoobarParams{
public FoobarParams(string alpha, int omega){...};
}
//fetch an instance:
FoobarParams parms = new FoobarParams("one",2);
//Unity 2.0 pseudo-code...
myContainer.resolve<IFoobar>(new parameterOverride(parms) );
При таком подходе я на полпути восстановил свой смысл. Но я должен подождать до времени выполнения, чтобы обнаружить ошибку, где я мог бы забыть предоставить параметр «FoobarParams».
Итак, давайте попробуем новый подход:
//code named "concrete creator"
class Foobar : IFoobar {
public Foobar(string alpha, int omega, ILogger log){...};
static IFoobar Create(string alpha, int omega){
//unity 2.0 pseudo-ish code. Assume a common
//service locator, or singleton holds the container...
return Container.Resolve<IFoobar>(
new parameterOverride[] {{"alpha", alpha},{"omega", omega} }
);
}
//Get my instance:
Foobar.Create("alpha",2);
На самом деле я не против, чтобы я использовал конкретный класс "Foobar" для создания IFoobar. Он представляет базовую концепцию , которую я не ожидаю изменить в своем коде. Я также не против отсутствия безопасности типов в статическом «Create», потому что теперь он инкапсулирован. Мой intellisense тоже работает! Любой конкретный экземпляр, созданный таким образом, будет игнорировать предоставленные параметры состояния, если они не применяются (поведение Unity 2.0). Возможно, другая конкретная реализация "FooFoobar" может иметь формальное несоответствие имени аргумента, но я все еще доволен этим.
Но большая проблема с этим подходом состоит в том, что он эффективно работает только с Unity 2.0 (несоответствующий параметр в Структурной карте вызовет исключение). Так что это хорошо, только если я останусь с Unity. Проблема в том, что мне начинает гораздо больше нравиться структура карты. Так что теперь я перехожу к еще одному варианту:
class Foobar : IFoobar, IFoobarInit {
public Foobar(ILogger log){...};
public IFoobar IFoobarInit.Initialize(string alpha, int omega){
this.alpha = alpha;
this.omega = omega;
return this;
}
}
//now create it...
IFoobar foo = myContainer.resolve<IFoobarInit>().Initialize("one", 2)
Теперь с этим у меня есть несколько приятных компромиссов с другими подходами: (1) Мои аргументы безопасны по типу / осведомлены о intellisense (2) У меня есть выбор получения ILogger через DI (показанный выше) или сервис locator, (3) нет необходимости создавать один или несколько отдельных конкретных классов FoobarFactory (в отличие от подробного кода-примера «более подробно») и (4) он разумно поддерживает принцип «сделать интерфейсы простыми в использовании, правильно, и трудно использовать неправильно ". По крайней мере, это, возможно, не хуже, чем ранее обсужденные альтернативы.
Еще остается один приемочный барьер: я также хочу применить «дизайн по контракту».
Каждый образец, который я представил, преднамеренно одобрял внедрение конструктора (для зависимостей состояний), потому что я хочу сохранить "инвариантную" поддержку, как это обычно практикуется. А именно, инвариант устанавливается, когда конструктор завершает работу.
В приведенном выше примере инвариант не устанавливается после завершения строительства объекта. Пока я занимаюсь «разработкой по контракту», я могу просто сказать разработчикам не проверять инвариант, пока не будет вызван метод Initialize (...).
Но что еще важнее, когда выйдет .net 4.0, я хочу использовать его поддержку "контрактного кода" для проектирования по контракту. Из того, что я прочитал, он не будет совместим с этим последним подходом.
Проклятие!
Конечно, мне также приходит в голову, что вся моя философия выключена. Возможно, мне скажут, что создание Foobar: IFoobar через локатор служб подразумевает, что это сервис - и сервисы имеют только другие сервисные зависимости, они не имеют зависимостей от состояния (таких как Alpha и Omega в этих примерах). Я также готов выслушать такие философские вопросы, но я также хотел бы знать, какую полуавтооритическую ссылку читать, чтобы направить меня на этот путь мысли.
Так что теперь я обращаюсь к сообществу. Какой подход я должен учитывать, что у меня еще нет? Должен ли я действительно верить, что исчерпал свои возможности?
p.s. Эта проблема, наряду с другими , побуждает меня полагать, что вся идея Контейнера IOC, по крайней мере, в .net, преждевременна. Это напоминает мне о тех днях, когда люди стояли на своих головах, чтобы язык «С» чувствовал себя объектно-ориентированным (добавляя странные макросы и тому подобное). Мы должны искать CLR и языковую поддержку контейнеров IOC. Например, представьте себе новый вид интерфейса, который называется «initiate». Инициат работает как интерфейс, но также требует определенной сигнатуры конструктора. Остальное оставляю ученику в качестве упражнения ...