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

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

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

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

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

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

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

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

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

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

Ответы [ 5 ]

267 голосов
/ 22 декабря 2009

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

Инициализация методов на интерфейсных запахах Leaky Abstraction .

В вашем случае я бы сказал, что вы должны смоделировать интерфейс 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 для вас, если вы зарегистрируете его правильно.

14 голосов
/ 22 декабря 2009

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

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

Наличие сеттеров или метода Initialize () обычно считается плохим дизайном, так как вам нужно "помнить", чтобы вызывать их и убедиться, что они не слишком открывают состояние вашей реализации (то есть что мешает кому-то повторно вызвать инициализацию или установщик?).

5 голосов
/ 11 июля 2013

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

1 голос
/ 22 декабря 2009

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

Я разделил 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. * * 1015

1 голос
/ 22 декабря 2009

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

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

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

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

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