Соответствует ли шаблон прототипа внедрению зависимостей?

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

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

public interface IAnalysis
{
    SomeDataType DoSomething();
    IAnalysis CreateObject();
}

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

Разумна ли концепция, которую я придумал? Есть ли другие решения, которые я могу использовать для достижения этой цели? На самом деле я думал об абстрактных фабриках, но в моем понимании это повредило бы принципу внедрения зависимостей.


person Paul Kertscher    schedule 08.10.2014    source источник
comment
Мне интересно, не путаете ли вы DI и IoC. Они связаны, но, конечно, не одно и то же. DI требует, чтобы зависимости объекта передавались извне, а не создавались внутри. В этом нет ничего, что мешало бы вам создать зависимость через конструктор ее типа.   -  person jmcilhinney    schedule 08.10.2014
comment
Нет, я имел в виду ДД. Я планирую иметь класс инжектора, который строит мой график зависимостей. Имеет ли это какое-то значение?   -  person Paul Kertscher    schedule 08.10.2014
comment
Re: Я читал, что при использовании внедрения зависимостей следует избегать конструкторов. Нет; следует избегать не самих конструкторов, а их прямого вызова через оператор new. Известно, что конструкторы очень полезны в DI (а именно для внедрения необходимых зависимостей).   -  person stakx - no longer contributing    schedule 08.10.2014
comment
@stakx Я, возможно, должен был написать [...] следует избегать вне инжектора. Конечно, они нужны, особенно для внедрения зависимостей.   -  person Paul Kertscher    schedule 08.10.2014
comment
@PaulKertscher: Это все равно было бы неточно. Я хотел указать, что конструкторы и выражения new … — это не одно и то же. (Последние создают новые объекты, используя первые; но new … выражения не являются конструкторами.) Совет, который вы прочитали, скорее всего, касался new (потому что только тогда он имеет смысл ), но вы цитируете это так: я читал, что конструкторы должны….   -  person stakx - no longer contributing    schedule 08.10.2014
comment
@stakx Теперь я понял твою точку зрения. Конечно, это не одно и то же, но имеет ли смысл эта строгая дифференциация, если нет возможности вызвать конструктор без new? Это означает, что следует избегать вызова конструкторов вне инжектора, если следует избегать создания объектов с new вне инжектора. Или есть возможность вызвать конструктор без нового?   -  person Paul Kertscher    schedule 08.10.2014
comment
Да. Вот альтернатива, которая имеет наибольшее значение в DI: вместо того, чтобы создавать зависимость самостоятельно с помощью new, вы позволяете другой стороне создать объект для вас — вуаля, вы больше не используете new самостоятельно! (Конечно, другая сторона может использовать new, но вашему коду не нужно об этом знать и не заботиться; важно иметь доступную зависимость. То, что кто-то другой создал ее, приведет вас обратно к корень композиции.) Эта идея лежит в основе IoC-аспекта DI: вы не new устанавливаете зависимости, когда и где бы они вам ни понадобились — вместо этого вы их получаете/внедряете.   -  person stakx - no longer contributing    schedule 08.10.2014
comment
(продолжение:) И именно в этом смысле вам следует избегать new (не конструкторов!): вам не следует разбрасывать new SomeDependency(…) по всей кодовой базе, а вместо этого концентрировать его в корне композиции (и, возможно, на фабриках, которые являются самими зависимостями, которые в идеале должны быть только new-ed в корне композиции). Также обратите внимание, что этот совет применим только к типам new, которые вы выбрали для внедрения (зависимости/компоненты/сервисы). Например, поскольку вы не хотите внедрять типы значений (такие как TimeSpan), new-введение этих допустимо.   -  person stakx - no longer contributing    schedule 08.10.2014
comment
(продолжение:) Связанный вопрос: При использовании внедрения зависимостей куда идут все новые операторы?   -  person stakx - no longer contributing    schedule 08.10.2014
comment
@stakx [...]и, возможно, на фабриках, которые сами по себе являются зависимостями, которые в идеале должны быть обновлены только в корне композиции[...] Теперь это приближается к моей первоначальной проблеме . Предположим, что у меня есть фабрика. Чем занимается этот завод? Разве эта фабрика не должна new создавать объекты, созданные фабрикой?   -  person Paul Kertscher    schedule 08.10.2014
comment
Опять же, избегайте new только для создания зависимостей. Если какой-то тип обладает внутренними знаниями о том, как построить объект, то использование new допустимо. Давайте посмотрим на FooFactory. Весь смысл существования фабрики заключается в том, что она заключает в себе знания о том, как построить объект; он не должен делегировать эти знания внешней стороне. Если бы FooFactory не знал, как создавать Foo объекты, это было бы лишним. Так что new Foo(…) здесь в порядке. Но если ctor Foo принимает дополнительные зависимости, ваша фабрика может также потребовать их, чтобы она могла их пересылать.   -  person stakx - no longer contributing    schedule 08.10.2014
comment
(продолжение:) Теперь к вашему случаю, шаблону прототипа: ваш метод CreateObject по существу предназначен для создания клона экземпляра. Поэтому какой бы тип ни реализовывал этот интерфейс, он должен иметь внутреннее знание того, как клонировать самого себя; это требование вытекает из намерения вашего интерфейса. Итак, опять же, использование new TypeThatImplementsTheInterface(…) здесь допустимо.   -  person stakx - no longer contributing    schedule 08.10.2014
comment
Кстати. извините за много длинных комментариев. Я, вероятно, должен преобразовать их в ответ. Как вы думаете, полезны ли они вам вообще?   -  person stakx - no longer contributing    schedule 08.10.2014
comment
Не важно, но жаль, что я не могу проголосовать ваши комментарии как ответ на мой вопрос :) Да, ваши объяснения были полезны, большое спасибо   -  person Paul Kertscher    schedule 08.10.2014


Ответы (3)


Я читал, что при использовании внедрения зависимостей следует избегать создания объектов с new вне инжектора […].

Это верно лишь отчасти. Я покажу вам шаг за шагом, что new имеет свое место, и что вполне можно использовать new для реализации шаблона прототипа.

Начнем с очевидного: если нам нужен экземпляр типа B, то он должен быть кем-то где-то создан. Допустим, у нас есть это:

class C
{
    void Baz()
    {
        B b = new B(new A(…));
        b.Bar();
    }
}

Baz требует B для выполнения своей работы. Если мы хотим избежать new B(…), лучшее, что мы можем сделать, это удалить его из этого конкретного места в кодовой базе:

class C
{
    C(Func<B> newB) // instead of Func<B>, we could also inject a B directly
    {               // (the difference being that we would no longer control
        this.newB = newB;                        // when the B gets created)
    }

    Func<B> newB;

    void Baz()
    {
        var b = newB();
        b.Bar();
    }
}

Но B, передаваемый конструктору C, все равно должен быть где-то создан. Только теперь это где-то в другом месте.

Итак, что мы получили, избегая new? C больше не требуется иметь внутренние знания о том, как именно создать B.

Но как Func<B> newB (т.е. фабричный метод) сам создаст B без использования new? Кажется, мы не можем вечно уклоняться от new.

Чтобы прояснить этот момент, давайте перейдем к другому, очень связанному примеру, который немного ближе к вашей проблеме (реализация шаблона прототипа в контексте DI): Абстрактные фабрики, еще один шаблон проектирования. Допустим, у нас есть BFactory, единственной обязанностью которого является создание экземпляров типа B:

interface BFactory
{
    B CreateB();
}

Можем ли мы реализовать это без использования new? Попробуем так же, как описано выше:

class RedundantBFactory : BFactory
{
    RedundantBFactory(Func<B> newB)
    {
        this.newB = newB;
    }

    Func<B> newB;

    public B CreateB()
    {
        return newB();
    }
}

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

Мы можем заключить, что разумно и уместно использовать new внутри абстрактных фабрик и фабричных методов (например, в BFactory или даже newB выше), если мы не хотим, чтобы они были полностью избыточны:

class UsefulBFactory : BFactory
{
    public UsefulAFactory(Func<A> newA)
    {
        this.newA = newA;
    }

    Func<A> newA;

    public B CreateB()
    {
        return new B(newA());
    }
}

Теперь к вашему шаблону прототипа: шаблон прототипа, по сути, касается клонирования объектов. То есть все типы, реализующие ваш интерфейс IAnalysis, должны иметь возможность создавать клоны (копии) экземпляра. Как и в приведенном выше примере с абстрактной фабрикой, единственной целью вашего интерфейса является инкапсуляция некоторой формы создания объекта. В первую очередь это причина его существования, поэтому классы, реализующие этот интерфейс, не должны делегировать эту ответственность внешней стороне. Опять же, в этом случае вполне разумно использовать new:

class W : IAnalysis
{
    W(X x, Y y, …)
    {
        this.x = x;
        this.y = y;
        …
    }

    public IAnalysis CreateObject()
    {
        return new W(x, y, …);
    }
}

И последнее замечание, просто чтобы подчеркнуть и завершить мое первоначальное заявление о том, что избегать new не во всех случаях имеет смысл: обратите внимание, что в любом случае DI не следует использовать для всего.

Обычно вам нужно принять решение о том, какие типы должны обрабатываться контейнером внедрения зависимостей. Эти так называемые зависимости, или компоненты, или службы обычно абстрагируются как interface или abstract class BaseClass, так что вы, возможно, позже сможете заменить одну реализацию другой. Единственное место, где вы используете new Service(…), должно быть в корне композиции или (как показано выше) в абстрактных фабриках или фабричных методах (которые сами по себе являются зависимостями, которые будут внедряться туда, где вам нужно создавать объекты в выбранное вами время). Если бы вы щедро разбросали new Service(…) по всему коду, было бы трудно заменить одну реализацию другой.

Но совершенно нормально использовать new для создания примитивных значений и экземпляров типов значений (таких как string, TimeSpan и т. д.). Эти типы обычно не создаются контейнером внедрения зависимостей.

person stakx - no longer contributing    schedule 08.10.2014

Я читал, что при использовании DI следует избегать конструкторов, поэтому я не совсем уверен, разрешено ли это в DI.

Это работает не так, но конструкторы — это деталь реализации реализации, и поскольку потребители знают только об абстракции, они не могут вызвать конструктор.

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

Помня об этом, классы можно создавать с использованием ключевого слова new, но когда дело доходит до внедрения конструкторы, вы должны хранить это создание локально в корне композиции. Это можно сделать, определив реализацию IAnalysis внутри корня композиции. Таким образом, остальная часть приложения не должна зависеть от этого конкретного типа и его конструктора. Если вы используете библиотеку DI, эта реализация может зависеть от контейнера и вызывать его для запроса нового экземпляра.

person Steven    schedule 08.10.2014
comment
Я понимаю, что потребители не знают о конструкторах и, следовательно, не смогут их вызывать, но я боюсь, что не понимаю, как это связано с моим вопросом об использовании шаблона прототипа и вызове конструктора изнутри< /i> реализующий класс. - person Paul Kertscher; 08.10.2014

Я думаю, что концепция внедрения зависимостей заключается в том, что вы внедряете что-то в другой класс. Теперь, почему вы хотите использовать внедрение зависимостей, из-за концепции ООП, и класс должен использовать другой класс (концепцию зависимости).

Теперь концепция внедрения здесь означает, что вы должны что-то внедрить в класс. Как вы вводите его? Поместив его в параметр.

Вы должны создать объект вне интерфейса, а затем передать объект/класс в качестве параметра объекту интерфейса.

Итак, вместо того, чтобы пытаться создать объект службы, почему бы вам не передать любой параметр или объект, который вам нужно обработать, и интерфейс вернет вам результат?

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

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

Спасибо,

person Jedi    schedule 08.10.2014
comment
Очень забавный пример, но не совсем подходящий ответ на мой вопрос. Говоря в терминах вашего примера, я бы не хотел нанимать одного киллера из фиксированного пула киллеров, но я хотел бы создавать киллеров по требованию, потому что я не знаю, сколько киллеров мне понадобится (не делает смысл однако). - person Paul Kertscher; 08.10.2014
comment
ммм.. ты богат.. В любом случае, ты не можешь просить компанию наемных убийц представить их список наемных убийц. Это правило игры. Вы можете, однако, спросить другую компанию-провайдера, кто здесь создает компанию-киллера (которая может быть фабричным классом)? - person Jedi; 08.10.2014