Параметры состояния обработки контейнера IOC в конструкторе не по умолчанию - PullRequest
4 голосов
/ 17 апреля 2010

Для целей этого обсуждения конструктор объекта может принимать два вида параметров: зависимость от состояния или зависимость от службы. Обеспечить служебную зависимость с контейнером 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». Инициат работает как интерфейс, но также требует определенной сигнатуры конструктора. Остальное оставляю ученику в качестве упражнения ...

Ответы [ 2 ]

3 голосов
/ 17 апреля 2010

То, что вы ищете, это «заводской адаптер» или Func .

Компонент, который должен создавать параметризованные экземпляры другого компонента на лету, использует тип зависимости Func для представления этого:

class Bar
{
    Func<string, int, IFoo> _fooCreator;

    public Bar(Func<string, int, IFoo> fooCreator) {
        _fooCreator = fooCreator;
    }

    public void Go() {
        var foo = _fooCreator("a", 42);
        // ...
    }
}

Например, Autofac автоматически предоставляет функцию при каждой регистрации IFoo. Он также объединит параметры в конструкторе Foo (включая ILogger,) и отбросит все ненужные, а не выдаст ошибку.

Autofac также поддерживает пользовательские делегаты, такие как:

delegate IFoo FooCreator(string alpha, int omega);

Таким образом, Bar может быть переписан:

class Bar
{
    FooCreator _fooCreator;

    public Bar(FooCreator fooCreator) {
        _fooCreator = fooCreator;
    }

    public void Go() {
        var foo = _fooCreator("a", 42);
        // ...
    }
}

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

Autofac имеет некоторую документацию, которую вы можете проверить: http://code.google.com/p/autofac/wiki/DelegateFactories.

Существует совместный проект между несколькими сообществами контейнеров IOC, который называется «адаптеры общего контекста» для стандартизации этих и других типов зависимостей более высокого порядка. Сайт проекта находится по адресу http://cca.codeplex.com.

Первая предложенная спецификация от CCA охватывает заводской адаптер, вы можете прочитать его здесь: http://cca.codeplex.com/wikipage?title=FuncFactoryScenarios&referringTitle=Documentation.

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

http://nblumhardt.com/2010/04/introducing-autofac-2-1-rtw/ http://nblumhardt.com/2010/01/the-relationship-zoo/

Надеюсь, вы обнаружите, что хотя контейнерам IoC, по общему признанию, предстоит пройти долгий путь, прежде чем они станут полностью прозрачными, мы на самом деле работаем довольно усердно, чтобы туда добраться:)

Ник

0 голосов
/ 17 апреля 2010

Если эти параметры постоянны на протяжении всего жизненного цикла приложения, вы можете добавить IConfigurationService, единственная цель которого - возвращать эти параметры всем, кто в них нуждается. Реализация IConfigurationService может иметь жестко закодированные значения, читать их из файла конфигурации ... что угодно. Конечно, реализация IConfigurationService извлекается через контейнер IoC.

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

Либо сделайте их настраиваемыми на самом объекте, предоставив метод установки, (который подходит, когда они могут измениться за время существования объекта), либо сделайте их параметры фабричным методом, который возвращает объект (где фабричный объект зарегистрирован в вашем контейнере IoC).

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...