Почему используется инъекция зависимостей?

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

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

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

Ответ 1

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

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

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

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

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

До сих пор так хорошо.

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

var logger = new Logger();

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

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

var logger = new Logger();

по:

var logger = new TcpLogger();

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

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

ICanLog logger = new Logger();

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

ICanLog logger = LoggerFactory.Create();

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

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

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

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

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

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

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

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

При инъекции зависимостей ваш класс Database теперь имеет конструктор, для которого требуется параметр типа ICanLog:

public Database(ICanLog logger) { ... }

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

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

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

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

Но в основном, что он.

Надеюсь, что это поможет.

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

Ответ 2

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

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

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

Ответ 3

Как указывалось в других ответах, инъекция зависимостей - это способ создания зависимостей вне класса, который его использует. Вы вводите их извне и берете контроль над их творчеством изнутри вашего класса. Именно поэтому инъекция зависимостей представляет собой реализацию принципа Inversion of control (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, он создает вам объект Object следующим образом:

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);
}

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

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

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

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

Ответ 4

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

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

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;
        }
    }
}

Теперь представьте себе, что теперь вам нужно будет поддерживать весь этот код в одном классе, потому что он не отделяется должным образом, вы можете себе представить, что для каждого нового процессора, который вы будете поддерживать, вам нужно создать новый, если//коммутатор для каждого метода, это только усложняется, однако, с помощью 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();
}

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

Ответ 5

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

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

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