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

Я пытаюсь заставить 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 ответов

Решение

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

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

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

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

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

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

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

Мне понравилось, как расширение 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 больше документации.

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

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

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

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

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

Я разделил IMyIntf в "геттер" и "сеттер" интерфейсы. Так:

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() все еще может быть вызван несколько раз, но используя биты парадигмы ServiceLocator, мы можем довольно просто обернуть его так, чтобы IMyIntfSetter почти внутренний интерфейс, который отличается от IMyIntf,