Чи існує шаблон для ініціалізації об'єктів, створених через контейнер DI


147

Я намагаюся змусити Unity керувати створенням моїх об'єктів, і я хочу мати параметри ініціалізації, які не відомі до часу виконання:

На даний момент єдиний спосіб, коли я міг би придумати спосіб, як це зробити, це мати метод Init в інтерфейсі.

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

Тоді для його використання (в Єдності) я би зробив це:

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

У цьому сценарії runTimeParamпараметр визначається під час виконання на основі введення користувача. Тривіальний випадок тут просто повертає значення, runTimeParamале насправді параметр буде чимось на зразок імені файлу, і метод ініціалізації буде щось робити з файлом.

Це створює ряд питань, а саме те, що Initializeметод доступний в інтерфейсі і його можна викликати кілька разів. Встановлення прапора в реалізації та викидання виключення при повторному виклику Initializeздається способом незграбним.

У той момент, коли я вирішую свій інтерфейс, я не хочу нічого знати про реалізацію IMyIntf. Однак я хочу знати, що цей інтерфейс потребує певних параметрів одноразової ініціалізації. Чи є спосіб якось анотувати (атрибути?) Інтерфейс з цією інформацією та передати їх у рамки, коли об’єкт створений?

Редагувати: Описаний інтерфейс трохи більше.


9
Вам не вистачає точки використання контейнера DI. Залежності повинні бути вирішені за вас.
П’єртен

Звідки ви берете потрібні параметри? (конфігураційний файл, db, ??)
Jaime,

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
Крім того, метод ініціалізації вказує на тимчасове з'єднання: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Марк

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.


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


13
Це не особливо вдале рішення, оскільки воно спирається на метод Initialize, який є непрохідною абстракцією. До речі, це не схоже на Локатор обслуговування, а більше нагадує Інжекцію інтерфейсу. У будь-якому випадку, див. Мою відповідь для кращого рішення.
Марк Семанн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.