Почему один использует внедрение зависимости?

536

Я пытаюсь понять инъекции зависимости (DI), и снова я потерпел неудачу. Это просто кажется глупым. Мой код никогда не беспорядок; Я почти не пишу виртуальные функции и интерфейсы (хотя я делаю это однажды в голубой луне), и вся моя конфигурация волшебным образом сериализуется в класс с использованием json.net (иногда с использованием сериализатора XML).

Я не совсем понимаю, какую проблему это решает. Это выглядит как способ сказать: «Привет. Когда вы запускаете эту функцию, возвращаете объект этого типа, который использует эти параметры / данные».
Но ... зачем мне это использовать? Заметьте, мне также никогда не приходилось пользоваться object, но я понимаю, для чего это нужно .

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

TJ Crowder
источник
3
Это также может быть полезной информацией: martinfowler.com/articles/injection.html
ta.speot.is
1
Смотрите также .
BlueRaja - Дэнни Пфлюгофт
41
Я с этим парнем: jamesshore.com/Blog/Dependency-Injection-Demystified.html .
Эрик Липперт
5
Другой очень простое объяснение DI: codearsenal.net/2015/03/...
ybonda

Ответы:

840

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

Интерфейсы являются прилагательными; классы существительные.

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

Так, например, интерфейс может быть чем-то вроде IDisposable, IEnumerableили IPrintable. Класс - это фактическая реализация одного или нескольких из этих интерфейсов: Listили они Mapмогут быть обеими реализациями IEnumerable.

Чтобы понять: часто ваши занятия зависят друг от друга. Например, у вас может быть Databaseкласс, который обращается к вашей базе данных (ха, сюрприз! ;-)), но вы также хотите, чтобы этот класс регистрировал доступ к базе данных. Предположим, у вас есть другой класс Logger, затем Databaseесть зависимость от Logger.

Все идет нормально.

Вы можете смоделировать эту зависимость внутри вашего Databaseкласса с помощью следующей строки:

var logger = new Logger();

и все хорошо. Это хорошо до того дня, когда вы поймете, что вам нужно несколько регистраторов: иногда вы хотите войти в консоль, иногда в файловую систему, иногда используя TCP / IP и удаленный сервер журналирования, и так далее ...

И, конечно же, вы НЕ хотите менять весь свой код (пока у вас есть его миллиарды) и заменять все строки

var logger = new Logger();

по:

var logger = new TcpLogger();

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

Очевидно, что неплохо было бы представить интерфейс ICanLog(или аналогичный), который реализован всеми различными регистраторами. Итак, шаг 1 в вашем коде - это то, что вы делаете:

ICanLog logger = new Logger();

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

ICanLog logger = LoggerFactory.Create();

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

Теперь, конечно, вы можете обобщить эту фабрику и заставить ее работать для любого типа:

ICanLog logger = TypeFactory.Create<ICanLog>();

Где-то эта TypeFactory нуждается в данных конфигурации, какой фактический класс должен быть создан при запросе определенного типа интерфейса, поэтому вам необходимо сопоставление. Конечно, вы можете сделать это отображение внутри вашего кода, но тогда изменение типа означает перекомпиляцию. Но вы также можете поместить это отображение в файл XML, например. Это позволяет вам изменять фактически используемый класс даже после времени компиляции (!), Что означает динамически, без перекомпиляции!

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

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

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

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

С внедрением зависимостей ваш Databaseкласс теперь имеет конструктор, который требует параметр типа ICanLog:

public Database(ICanLog logger) { ... }

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

И вот здесь вступает в игру инфраструктура DI: вы снова настраиваете свои отображения, а затем просите свою инфраструктуру DI создать для вас экземпляр приложения. Поскольку Applicationкласс требует ICanPersistDataреализации, экземпляр Databaseвнедряется - но для этого он должен сначала создать экземпляр типа регистратора, для которого настроен ICanLog. И так далее ...

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

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

Но в основном это все.

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

Голо Роден
источник
7
Конечно, вы можете сделать это и таким образом, но тогда вам придется реализовать эту логику в каждом отдельном классе, который обеспечит поддержку взаимозаменяемости реализации. Это означает много дублированного, избыточного кода, а также означает, что вам нужно прикоснуться к существующему классу и переписать его частично, как только вы решите, что он вам нужен сейчас. DI позволяет вам использовать это в любом произвольном классе, вам не нужно писать их особым образом (кроме определения зависимостей в качестве параметров в конструкторе).
Голо Роден
138
Вот что я никогда не понимаю в DI: это значительно усложняет архитектуру . И все же, на мой взгляд, использование довольно ограничено. Примеры, конечно, всегда одинаковы: сменные регистраторы, сменная модель / доступ к данным. Иногда сменный взгляд. Но это все. Означают ли эти несколько случаев действительно более сложную программную архитектуру? - Полное раскрытие: я уже использовал DI с большим эффектом, но это было для очень специальной архитектуры плагинов, из которой я не буду обобщать.
Конрад Рудольф
17
@GoloRoden, почему вы вызываете интерфейс ICanLog вместо ILogger? Я работал с другим программистом, который часто делал это, и я никогда не мог понять соглашение? Для меня это похоже на вызов IEnumerable ICanEnumerate?
DermFrench
28
Я назвал это ICanLog, потому что мы слишком часто работаем со словами (существительными), которые ничего не значат. Например, что такое брокер? Менеджер? Даже Репозиторий не определен уникальным образом. И наличие всех этих вещей как существительных является типичной болезнью ОО языков (см. Steve-yegge.blogspot.de/2006/03/… ). Что я хочу сказать, так это то, что у меня есть компонент, который может вести запись для меня - так почему бы не назвать его так? Конечно, это также игра с I как от первого лица, следовательно, ICanLog (ForYou).
Голо Роден
18
@ Давид Модульное тестирование работает просто отлично - в конце концов, юнит независим от других вещей (иначе это не юнит). То, что не работает без DI-контейнеров, это фиктивное тестирование. Справедливости ради, я не уверен, что польза от насмешек перевешивает сложность добавления DI-контейнеров во всех случаях. Я делаю строгие юнит-тесты. Я редко делаю насмешки.
Конрад Рудольф
499

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

Внедрение зависимостей - очень простая концепция. Вместо этого кода:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

Вы пишете код так:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

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

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

Даниэль Приден
источник
7
Почему я предпочел бы второй код, а не первый? у первого только новое ключевое слово, как это помогает?
user962206
17
@ user962206 подумай, как бы ты проверил A независимо от B
jk.
66
@ user962206, также подумайте о том, что произойдет, если B понадобится несколько параметров в своем конструкторе: чтобы создать его экземпляр, A должен был бы знать об этих параметрах, то, что может быть совершенно не связано с A (он просто хочет зависеть от B , не от чего зависит B). Передача уже сконструированного B (или любого подкласса или макета B в этом отношении) конструктору A решает это и заставляет A зависеть только от B :)
Эпидемиан
17
@ acidzombie24: Как и многие шаблоны проектирования, DI не очень полезен, если ваша кодовая база не достаточно велика, чтобы простой подход стал проблемой. Мне кажется, что DI не будет улучшением, пока ваше приложение не будет иметь более 20 000 строк кода и / или более 20 зависимостей от других библиотек или сред. Если ваше приложение меньше этого, вы все равно можете предпочесть программирование в стиле DI, но разница будет не такой существенной.
Даниэль Приден
2
@DanielPryden Я не думаю, что размер кода так же важен, как то, насколько динамичен ваш код. если вы регулярно добавляете новые модули, которые соответствуют одному и тому же интерфейсу, вам не придется менять зависимый код так часто.
FistOfFury
35

Как говорилось в других ответах, внедрение зависимостей - это способ создания ваших зависимостей вне класса, который их использует. Вы вводите их извне и контролируете их создание изнутри вашего класса. Именно поэтому внедрение зависимостей является реализацией принципа инверсии управления (IoC).

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

Моя особенность:

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

Вы можете проверить это так:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

Так что где-то в OfferWeasel , он строит вам объект предложения, как это:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

Проблема здесь в том, что этот тест, скорее всего, всегда будет неудачным, поскольку устанавливаемая дата будет отличаться от заявленной, даже если вы просто DateTime.Now введете код теста, он может быть отключен на пару миллисекунд и поэтому всегда терпеть неудачу. Лучшим решением сейчас было бы создать интерфейс для этого, который позволит вам контролировать, какое время будет установлено:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

Интерфейс - это абстракция. Одна из них - РЕАЛЬНАЯ вещь, а другая позволяет вам подделывать какое-то время там, где это необходимо. Затем тест можно изменить следующим образом:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Таким образом, вы применили принцип «инверсии управления», введя зависимость (получая текущее время). Основная причина сделать это для облегчения изолированного модульного тестирования, есть другие способы сделать это. Например, интерфейс и класс здесь не нужны, поскольку в C # функции могут передаваться как переменные, поэтому вместо интерфейса вы можете использовать a Func<DateTime>для достижения того же самого. Или, если вы используете динамический подход, вы просто передаете любой объект, который имеет эквивалентный метод ( типизирование по типу утки ), и вам вообще не нужен интерфейс.

Вам вряд ли понадобится более одного регистратора. Тем не менее, внедрение зависимостей необходимо для статически типизированного кода, такого как Java или C #.

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

Я надеюсь, что это помогло.

cessor
источник
4
Это на самом деле выглядит как ужасное решение. Я определенно написал бы код, более похожий на предложенный Дэниелом Приденом, но для этого конкретного модульного теста я бы просто сделал DateTime.Now до и после функции и проверил, находится ли время между ними? Добавление большего количества интерфейсов / намного больше строк кода кажется мне плохой идеей.
3
Мне не нравятся общие примеры A (B), и я никогда не чувствовал, что регистратор должен иметь 100 реализаций. Это пример, с которым я недавно столкнулся, и это один из 5 способов его решения, где один из них фактически включен с использованием PostSharp. Это иллюстрирует классический подход на основе инъекций ctor. Не могли бы вы привести лучший реальный пример того, где вы столкнулись с хорошим использованием DI?
cessier
2
Я никогда не видел хорошего использования для DI. Вот почему я написал вопрос.
2
Я не нашел это полезным. Мой код всегда легко сделать тест. Кажется, DI хорош для больших баз кода с плохим кодом.
1
Помните, что даже в небольших функциональных программах всякий раз, когда у вас есть f (x), g (f), вы уже используете внедрение зависимостей, поэтому каждое продолжение в JS будет считаться внедрением зависимости. Я предполагаю, что вы уже используете его;)
Кессор
15

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

Например, мы являемся центральным поставщиком платежей, работающим со многими поставщиками платежей по всему миру. Однако, когда сделан запрос, я понятия не имею, какому платежному процессору я собираюсь позвонить. Я мог бы запрограммировать один класс с кучей переключателей, таких как:

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

Теперь представьте, что теперь вам нужно хранить весь этот код в одном классе, потому что он не отделен должным образом, вы можете представить, что для каждого нового процессора, который вы будете поддерживать, вам нужно будет создать новый регистр if // для с каждым методом это только усложняется, однако, используя Dependency Injection (или Inversion of Control - как его иногда называют, что означает, что тот, кто контролирует выполнение программы, известен только во время выполнения, а не усложняется), вы можете достичь чего-то очень аккуратный и ремонтопригодный.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** код не скомпилируется, я знаю :)

Итай Саги
источник
+1, потому что звучит так, будто вы говорите, что вам это нужно там, где вы используете виртуальные методы / интерфейсы. Но это все еще редкость. Я бы все-таки передал его, new ThatProcessor()а не использовал бы фреймворк
@ItaiS Вы можете избежать бесчисленных переключателей с помощью шаблона проектирования класса фабрики. Используйте отражение System.Reflection.Assembly.GetExecutingAssembly (). CreateInstance ()
domenicr
@ domenicr конечно! но я хотел объяснить это в упрощенном примере
Итай Саги
Я согласен с приведенным выше объяснением, кроме необходимости заводского класса. В тот момент, когда мы реализуем фабричный класс, это просто грубый выбор. Лучшее объяснение выше, которое я нашел в главе «Пойморфизм и виртуальная функция» Брюса Эркеля. Истинный DI должен быть свободен от выбора, а тип объекта должен определяться во время выполнения через интерфейс автоматически. Это также то, что является истинным полиморфным поведением.
Арвинд Крмар
Например (согласно c ++) у нас есть общий интерфейс, который берет только ссылку на базовый класс и реализует поведение его производного класса без выбора. void tune (Instrument & i) {i.play (middleC); } int main () {Флейта ветра; мелодия (флейта); } Инструмент - это базовый класс, из него происходит ветер. Согласно c ++, виртуальные функции позволяют реализовать поведение производного класса через общий интерфейс.
Арвинд Крмар
6

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

Это уже часто встречается в ООП. Много раз создавая такие фрагменты кода, как:

I_Dosomething x = new Impl_Dosomething();

Недостатком является то, что класс реализации все еще жестко закодирован, следовательно, имеет интерфейс, который знает, какая реализация используется. DI продвигает проектирование через интерфейс еще на один шаг, и единственное, что нужно знать интерфейсу, - это знание интерфейса. Между DYI и DI находится шаблон локатора службы, поскольку внешний интерфейс должен предоставить ключ (присутствующий в реестре локатора службы), чтобы его запрос стал разрешенным. Пример поиска службы:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

Пример DI:

I_Dosomething x = DIContainer.returnThat();

Одним из требований DI является то, что контейнер должен иметь возможность выяснить, какой класс является реализацией какого интерфейса. Следовательно, для контейнера DI требуется строго типизированный дизайн и только одна реализация для каждого интерфейса одновременно. Если вам нужно больше реализаций интерфейса в одно и то же время (например, калькулятор), вам нужен сервисный локатор или шаблон проектирования фабрики.

D (b) I: Внедрение зависимостей и проектирование по интерфейсу. Это ограничение не является большой практической проблемой. Преимущество использования D (b) I заключается в том, что он служит для связи между клиентом и поставщиком. Интерфейс - это перспектива объекта или набора поведений. Последнее имеет решающее значение здесь.

Я предпочитаю администрирование сервисных контрактов вместе с D (b) I в кодировании. Они должны идти вместе. Использование D (b) I в качестве технического решения без организационного администрирования сервисных контрактов, на мой взгляд, не очень выгодно, потому что DI - это просто дополнительный уровень инкапсуляции. Но когда вы можете использовать это вместе с организационным администрированием, вы действительно можете использовать организационный принцип D (b), который я предлагаю. Это может помочь вам в долгосрочной перспективе структурировать взаимодействие с клиентом и другими техническими отделами по таким темам, как тестирование, управление версиями и разработка альтернатив. Когда у вас есть неявный интерфейс, как в жестко закодированном классе, он становится намного менее общительным со временем, чем когда вы делаете его явным, используя D (b) I. Все сводится к обслуживанию, которое происходит со временем, а не за один раз. :-)

Люк Бергман
источник
1
«Недостаток в том, что класс реализации все еще жестко закодирован» <- большую часть времени существует только одна реализация, и, как я сказал, я не могу думать о неигровом коде, для которого требуется интерфейс, который еще не встроен (.NET ).
@ acidzombie24 Может быть ... но сравните усилия по внедрению решения с использованием DI с самого начала с усилиями по изменению решения без DI позже, если вам нужны интерфейсы. Я почти всегда выбирал первый вариант. Лучше заплатить сейчас 100 $, а не платить 100.000 $ завтра.
Голо Роден
1
@GoloRoden Действительно, техническое обслуживание является ключевым вопросом при использовании таких методов, как D (b) I. Это 80% стоимости приложения. Конструкция, в которой требуемое поведение делается явным образом с использованием интерфейсов с самого начала, впоследствии экономит организации много времени и денег.
Лок Бергман
Я не пойму по-настоящему, пока не буду платить, потому что до сих пор я заплатил 0 долларов, и до сих пор мне остается платить только 0 долларов. Но я плачу $ 0,05 за чистоту каждой строки или функции.