Есть ли шаблон для инициализации объектов, созданных через DI-контейнер

147

Я пытаюсь заставить Unity управлять созданием моих объектов, и я хочу иметь некоторые параметры инициализации, которые не известны до времени выполнения:

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

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Затем, чтобы использовать его (в Unity), я бы сделал это:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

В этом сценарии runTimeParamпараметр определяется во время выполнения на основе пользовательского ввода. Тривиальный случай здесь просто возвращает значение, runTimeParamно в действительности параметр будет выглядеть как имя файла, а метод initialize будет что-то делать с файлом.

Это создает ряд проблем, а именно то, что Initializeметод доступен на интерфейсе и может вызываться несколько раз. Установка флага в реализации и создание исключения при повторном вызове Initializeкажется неуклюжим.

В тот момент, когда я разрешаю свой интерфейс, я не хочу ничего знать о реализации IMyIntf. Однако я хочу знать, что этот интерфейс требует определенных параметров инициализации. Есть ли способ как-то аннотировать (атрибуты?) Интерфейс этой информацией и передавать их в каркас при создании объекта?

Редактировать: описал интерфейс немного подробнее.

Игорь Зевака
источник
9
Вы упускаете смысл использования контейнера DI. Зависимости должны быть решены для вас.
Пьерретен
Откуда вы получаете необходимые параметры? (файл конфигурации, дб, ??)
Хайме
runTimeParamэто зависимость, которая определяется во время выполнения на основе пользовательского ввода. Должна ли альтернатива для этого делиться на два интерфейса - один для инициализации, а другой для хранения значений?
Игорь Зевака
Зависимость в IoC, как правило, относится к зависимости от других классов или объектов типа ref, которые могут быть определены на этапе инициализации IoC. Если вашему классу для работы нужны только некоторые значения, вот где метод Initialize () в вашем классе становится удобным.
Свет
Я имею в виду, что в вашем приложении есть 100 классов, к которым можно применить этот подход; тогда вам нужно будет создать дополнительные 100 фабричных классов + 100 интерфейсов для ваших классов, и вы могли бы избежать неприятностей, если бы просто использовали метод Initialize ().
Свет

Ответы:

276

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

Инициализация методов на интерфейсах пахнет утечкой абстракции .

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

Таким образом, интерфейс должен быть просто:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Теперь определите абстрактную фабрику:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Теперь вы можете создать конкретную реализацию, IMyIntfFactoryкоторая создает конкретные экземпляры, IMyIntfподобные этой:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Обратите внимание, как это позволяет нам защитить инварианты класса с помощью readonlyключевого слова. Никаких вонючих методов инициализации не требуется.

IMyIntfFactoryРеализация может быть столь же простым , как это:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

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

Любой DI-контейнер, достойный своей соли, сможет автоматически подключить IMyIntfFactoryэкземпляр для вас, если вы зарегистрируете его правильно.

Марк Симанн
источник
13
Проблема в том, что метод (например, Initialize) является частью вашего API, а конструктор - нет. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Марк
13
Кроме того, метод Initialize указывает временную связь
Марк
2
@Darlene Вы можете использовать инициализатор с отложенной инициализацией, как описано в разделе 8.3.6 моей книги . Я также привожу пример чего-то похожего в своей презентации « Большие графы объектов» .
Марк Симанн
2
@Mark Если для создания MyIntfреализации фабрики требуется больше, чем runTimeParam(читай: другие службы, которые нужно разрешить с помощью IoC), то вы все равно сталкиваетесь с разрешением этих зависимостей на своей фабрике. Мне нравится ответ @PhilSandler о передаче этих зависимостей в конструктор фабрики, чтобы решить эту проблему - это ваше мнение ?
Джефф
2
Тоже отличный материал, но ваш ответ на этот другой вопрос действительно дошел до моей точки зрения.
Джефф
15

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

Если вам нужен контекстно-зависимый параметр, передаваемый в конструкторе, один из вариантов - создать фабрику, которая разрешает ваши служебные зависимости через конструктор и принимает ваш параметр времени выполнения в качестве параметра метода Create () (или Generate ( ), Build () или как вы называете ваши фабричные методы).

Обычно сеттеры или метод Initialize () считаются плохим проектом, так как вам нужно «помнить», чтобы вызывать их, и убедиться, что они не слишком открывают состояние вашей реализации (т. Е. Что мешает кому-либо Вызов инициализации или сеттера?).

Фил Сэндлер
источник
5

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

Мне понравилось, как расширение Ninject, которое позволяет динамически создавать фабрики на основе интерфейсов:

Bind<IMyFactory>().ToFactory();

Я не мог найти подобную функциональность непосредственно в Unity ; поэтому я написал свое собственное расширение для IUnityContainer, которое позволяет регистрировать фабрики, которые будут создавать новые объекты на основе данных из существующих объектов, по существу отображая из иерархии одного типа в иерархию другого типа: UnityMappingFactory @ GitHub

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

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Затем вы просто объявляете интерфейс фабрики отображения в конструкторе для CI и используете его метод Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

В качестве дополнительного бонуса любые дополнительные зависимости в конструкторе отображаемых классов также будут разрешаться при создании объекта.

Очевидно, это не решит всех проблем, но до сих пор мне это очень хорошо помогло, поэтому я решил поделиться этим. На сайте проекта на GitHub больше документации.

jigamiller
источник
1

Я не могу ответить с определенной терминологией Unity, но, похоже, вы только что узнали о внедрении зависимости. Если это так, я призываю вас прочитать краткое, понятное и информативное руководство пользователя для Ninject. .

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

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

Даже если вы не используете Ninject, пошаговое руководство предоставит вам концепции и терминологию функциональности, которая соответствует вашим целям, и вы сможете сопоставить эти знания с Unity или другими структурами DI (или убедить вас попробовать Ninject) ,

Энтони
источник
Спасибо за это. Я на самом деле оцениваю DI-фреймворки, и NInject станет моей следующей.
Игорь Зевака
@johann: провайдеры? github.com/ninject/ninject/wiki/…
Энтони
1

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

Я разделен IMyIntfна интерфейсы «getter» и «setter». Так:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Тогда реализация:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()все еще может вызываться несколько раз, но используя биты парадигмы Service Locator, мы можем довольно просто обернуть его так, что IMyIntfSetterэто почти внутренний интерфейс, отличный от IMyIntf.

Игорь Зевака
источник
13
Это не очень хорошее решение, так как оно опирается на метод Initialize, который является Leaky Abstraction. Кстати, это не похоже на Service Locator, но больше похоже на внедрение интерфейса. В любом случае, смотрите мой ответ для лучшего решения.
Марк Симанн