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

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

Изменить: описал интерфейс немного больше.

Ответы

Ответ 1

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

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

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

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

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Теперь определите абстрактный Factory:

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 для вас, если вы зарегистрируете его правильно.

Ответ 2

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

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

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

Ответ 3

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

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

Bind<IMyFactory>().ToFactory();

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

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

//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>();

Затем вы просто объявляете интерфейс отображения factory в конструкторе для 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.

Ответ 4

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

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

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

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

Ответ 5

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

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