Что такое внедрение зависимостей? - PullRequest
2885 голосов
/ 25 сентября 2008

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

Что такое внедрение зависимостей и когда / почему его следует или не следует использовать?

Ответы [ 34 ]

2213 голосов
/ 26 сентября 2008

Лучшее определение, которое я нашел, это от Джеймса Шора :

«Инъекция зависимости» - 25 долларов термин для концепции 5 центов. [...] Инъекция зависимости означает предоставление возьмите его переменные экземпляра. [...].

Есть статья Мартина Фаулера , которая также может оказаться полезной.

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

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

1816 голосов
/ 25 сентября 2008

Внедрение зависимостей передает зависимость другим объектам или framework (инжектор зависимости).

Внедрение зависимостей облегчает тестирование. Инъекция может быть сделана через конструктор .

SomeClass() имеет следующий конструктор:

public SomeClass() {
    myObject = Factory.getObject();
}

Задача : Если myObject включает сложные задачи, такие как доступ к диску или доступ к сети, hard выполнить модульное тестирование на SomeClass(). Программисты вынуждены издеваться myObject и могут перехватить заводской вызов.

Альтернативное решение :

  • Передача myObject в качестве аргумента конструктору
public SomeClass (MyClass myObject) {
    this.myObject = myObject;
}

myObject можно передать напрямую, что облегчает тестирование.

  • Одной из распространенных альтернатив является определение бездействующего конструктора . Внедрение зависимости может быть сделано через сеттеры. (h / t @MikeVella).
  • Мартин Фаулер описывает третью альтернативу (h / t @MarcDix), где классы явно реализуют интерфейс для внедрения зависимостей программистам.

Сложнее выделить компоненты при модульном тестировании без внедрения зависимостей.

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

576 голосов
/ 22 мая 2011

Я нашел этот забавный пример с точки зрения слабой связи :

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

Например, рассмотрим объект Car.

A Car зависит от колёс, двигателя, топлива, аккумулятора и т. Д. Традиционно мы определяем марку таких зависимых объектов вместе с определением объекта Car.

Без внедрения зависимостей (DI):

class Car{
  private Wheel wh = new NepaliRubberWheel();
  private Battery bt = new ExcideBattery();

  //The rest
}

Здесь объект Car отвечает за создание зависимых объектов.

Что если мы захотим изменить тип зависимого объекта - скажем, Wheel - после начальных NepaliRubberWheel() проколов? Нам нужно воссоздать объект Car с его новой зависимостью, скажем, ChineseRubberWheel(), но это может сделать только производитель Car.

Тогда что же нам Dependency Injection делает для ...?

При использовании внедрения зависимостей объектам присваиваются зависимости во время выполнения, а не во время компиляции (время изготовления автомобиля) . Так что теперь мы можем изменить Wheel, когда захотим. Здесь dependency (wheel) можно ввести в Car во время выполнения.

После использования внедрения зависимости:

Здесь мы вводим зависимостей (Колесо и батарея) во время выполнения. Отсюда и термин: Внедрение зависимостей.

class Car{
  private Wheel wh = // Inject an Instance of Wheel (dependency of car) at runtime
  private Battery bt = // Inject an Instance of Battery (dependency of car) at runtime
  Car(Wheel wh,Battery bt) {
      this.wh = wh;
      this.bt = bt;
  }
  //Or we can have setters
  void setWheel(Wheel wh) {
      this.wh = wh;
  }
}

Источник: Понимание внедрения зависимости

248 голосов
/ 25 сентября 2008

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

Например, рассмотрим эти предложения:

public class PersonService {
  public void addManager( Person employee, Person newManager ) { ... }
  public void removeManager( Person employee, Person oldManager ) { ... }
  public Group getGroupByManager( Person manager ) { ... }
}

public class GroupMembershipService() {
  public void addPersonToGroup( Person person, Group group ) { ... }
  public void removePersonFromGroup( Person person, Group group ) { ... }
} 

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

С внедрением Dependency Injection вместо создания экземпляра GroupMembershipService в вашем PersonService вы либо передаете его конструктору PersonService, либо добавляете свойство (getter и setter) для установки локального экземпляра Это. Это означает, что вашему PersonService больше не нужно беспокоиться о том, как создать GroupMembershipService, он просто принимает те, которые ему даны, и работает с ними. Это также означает, что все, что является подклассом GroupMembershipService или реализует интерфейс GroupMembershipService, может быть «внедрено» в PersonService, и PersonService не нужно знать об изменении.

155 голосов
/ 06 января 2011

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

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

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

115 голосов
/ 06 июля 2017

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

public class Car
{
    public Car()
    {
        GasEngine engine = new GasEngine();
        engine.Start();
    }
}

public class GasEngine
{
    public void Start()
    {
        Console.WriteLine("I use gas as my fuel!");
    }
}

И для создания экземпляра класса Car мы будем использовать следующий код:

Car car = new Car();

Проблема с этим кодом, который мы тесно связали с GasEngine, и если мы решим изменить его на ElectricityEngine, то нам нужно будет переписать класс Car. И чем больше приложение, тем больше проблем и головной боли нам придется добавить и использовать новый тип движка.

Другими словами, при таком подходе наш класс автомобилей высокого уровня зависит от класса GasEngine более низкого уровня, который нарушает принцип инверсии зависимости (DIP) от SOLID. DIP предполагает, что мы должны зависеть от абстракций, а не от конкретных классов. Чтобы удовлетворить это, мы вводим интерфейс IEngine и переписываем код, как показано ниже:

    public interface IEngine
    {
        void Start();
    }

    public class GasEngine : IEngine
    {
        public void Start()
        {
            Console.WriteLine("I use gas as my fuel!");
        }
    }

    public class ElectricityEngine : IEngine
    {
        public void Start()
        {
            Console.WriteLine("I am electrocar");
        }
    }

    public class Car
    {
        private readonly IEngine _engine;
        public Car(IEngine engine)
        {
            _engine = engine;
        }

        public void Run()
        {
            _engine.Start();
        }
    }

Теперь наш класс Car зависит только от интерфейса IEngine, а не от конкретной реализации движка. Теперь единственная хитрость заключается в том, как нам создать экземпляр Car и присвоить ему конкретный класс Engine, такой как GasEngine или ElectricityEngine. Вот где вводится зависимость .

   Car gasCar = new Car(new GasEngine());
   gasCar.Run();
   Car electroCar = new Car(new ElectricityEngine());
   electroCar.Run();

Здесь мы в основном внедряем (передаем) нашу зависимость (экземпляр Engine) в конструктор Car. Так что теперь наши классы имеют слабую связь между объектами и их зависимостями, и мы можем легко добавлять новые типы двигателей без изменения класса Car.

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

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

Также, когда у нас много зависимостей, очень полезно использовать контейнеры Inversion of Control (IoC), которые позволяют определить, какие интерфейсы должны быть отображены, какие конкретные реализации для всех наших зависимостей, и мы можем разрешить эти зависимости для нас. когда он строит наш объект. Например, мы могли бы указать в сопоставлении для контейнера IoC, что зависимость IEngine должна быть сопоставлена ​​с классом GasEngine и когда мы запрашиваем у контейнера IoC экземпляр нашего * 1039 Класс * Car , он автоматически создаст наш класс Car с передачей зависимости GasEngine .

ОБНОВЛЕНИЕ: Недавно посмотрел курс об EF Core от Джули Лерман, а также понравилось ее краткое определение о DI.

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

103 голосов
/ 22 октября 2012

Давайте представим, что вы хотите пойти на рыбалку:

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

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

88 голосов
/ 05 мая 2016

Это - самое простое объяснение Инъекции зависимостей и Контейнер инъекции зависимостей Я когда-либо видел:

Без внедрения зависимостей

  • Приложению требуется Foo (например, контроллер), поэтому:
  • Приложение создает Foo
  • Приложение вызывает Foo
    • Foo нуждается в Bar (например, услуга), поэтому:
    • Foo создает Bar
    • Foo вызывает Бар
      • Бар нуждается в Bim (сервис, хранилище, …), Поэтому:
      • Бар создает Бим
      • Бар что-то делает

с инъекцией зависимостей

  • Приложению требуется Foo, которому нужен Bar, которому нужен Bim, поэтому:
  • Приложение создает Bim
  • Приложение создает бар и дает ему Bim
  • Приложение создает Foo и дает ему Bar
  • Приложение вызывает Foo
    • Foo вызывает Бар
      • Бар что-то делает

Использование контейнера для ввода зависимостей

  • Приложение нуждается в Foo так:
  • Приложение получает Foo из контейнера, поэтому:
    • Контейнер создает Bim
    • Контейнер создает Бар и дает ему Бим
    • Контейнер создает Foo и дает ему Bar
  • Приложение вызывает Foo
    • Фу называет Бар
      • Бар что-то делает

Внедрение зависимостей и Внедрение зависимостей Контейнеры - это разные вещи:

  • Внедрение зависимостей - метод для написания лучшего кода
  • Контейнер DI - это инструмент, помогающий вводить зависимости

Вам не нужен контейнер для внедрения зависимостей. Однако контейнер может помочь вам.

51 голосов
/ 02 мая 2013

Разве «внедрение зависимостей» не означает просто использование параметризованных конструкторов и открытых сеттеров?

Статья Джеймса Шора показывает следующие примеры для сравнения .

Конструктор без внедрения зависимости:

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example() { 
    myDatabase = new DatabaseThingie(); 
  } 

  public void doStuff() { 
    ... 
    myDatabase.getData(); 
    ... 
  } 
} 

Конструктор с внедрением зависимостей:

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example(DatabaseThingie useThisDatabaseInstead) { 
    myDatabase = useThisDatabaseInstead; 
  }

  public void doStuff() { 
    ... 
    myDatabase.getData(); 
    ... 
  } 
}
36 голосов
/ 28 апреля 2015

Что такое инъекция зависимостей (DI)?

Как уже говорили другие, Внедрение зависимостей (DI) устраняет ответственность прямого создания и управления продолжительностью жизни других экземпляров объекта, от которых зависит наш класс интересов (потребительский класс) (в смысл UML ). Эти экземпляры вместо этого передаются в наш потребительский класс, как правило, в качестве параметров конструктора или через установщики свойств (управление экземпляром объекта зависимости и передачей в потребительский класс обычно выполняется контейнером Inversion of Control (IoC) ). , но это уже другая тема).

DI, DIP и SOLID

В частности, в парадигме Роберта С. Мартина SOLID принципы объектно-ориентированного проектирования , DI является одной из возможных реализаций принципа обращения зависимостей (DIP) . DIP - это D SOLID мантры - другие реализации DIP включают в себя Service Locator и шаблоны плагинов.

Цель DIP - развязать жесткие, конкретные зависимости между классами и вместо этого ослабить связь с помощью абстракции, которая может быть достигнута с помощью interface, abstract class или pure virtual class, в зависимости от на используемом языке и подходе.

Без DIP наш код (я назвал этот «класс потребления») напрямую связан с конкретной зависимостью и также часто обременен обязанностью знать, как получить и управлять экземпляром этой зависимости, т.е. концептуально:

"I need to create/use a Foo and invoke method `GetBar()`"

Принимая во внимание, что после применения DIP требование ослабляется, а проблема получения и управления продолжительностью жизни зависимости Foo была удалена:

"I need to invoke something which offers `GetBar()`"

Зачем использовать DIP (и DI)?

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

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

Это можно посмотреть по-разному:

  • Если необходимо сохранить контроль продолжительности жизни класса-потребителя, управление можно восстановить, добавив (абстрактную) фабрику для создания экземпляров класса зависимости в класс-потребитель. Потребитель сможет получать экземпляры через Create на заводе по мере необходимости и утилизировать эти экземпляры после завершения.
  • Или же управление продолжительностью жизни экземпляров зависимостей может быть передано контейнеру IoC (подробнее об этом ниже).

Когда использовать DI?

  • Там, где, вероятно, потребуется заменить зависимость эквивалентной реализацией,
  • В любое время, когда вам понадобится выполнить модульное тестирование методов класса в изоляции его зависимостей,
  • Там, где неопределенность продолжительности жизни зависимости может служить основанием для экспериментов (например, Эй, MyDepClass является поточно-ориентированным - что если мы сделаем его одиночным и добавим один и тот же экземпляр всем потребителям?)
* ** 1074 тысяча семьдесят три * Пример * * тысяча семьдесят шесть

Вот простая реализация на C #. Учитывая ниже класс потребления:

public class MyLogger
{
   public void LogRecord(string somethingToLog)
   {
      Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
   }
}

Несмотря на то, что он выглядит безобидным, он имеет две зависимости static от двух других классов, System.DateTime и System.Console, которые не только ограничивают параметры вывода журнала (вход в консоль будет бесполезен, если никто не наблюдает), но хуже того, что это сложно автоматически проверить, учитывая зависимость от недетерминированных системных часов.

Тем не менее, мы можем применить DIP к этому классу, абстрагировав внимание от временных отметок как зависимости и связав MyLogger только с простым интерфейсом:

public interface IClock
{
    DateTime Now { get; }
}

Мы также можем ослабить зависимость от Console до абстракции, такой как TextWriter. Внедрение зависимостей обычно реализуется как constructor внедрение (передача абстракции в зависимость в качестве параметра конструктору потребляющего класса) или Setter Injection (передача зависимости через установщик setXyz() или свойство .Net с {set;} определено). Внедрение в конструктор является предпочтительным, так как это гарантирует, что класс будет в правильном состоянии после создания, и позволяет полям внутренней зависимости помечаться как readonly (C #) или final (Java). Таким образом, используя инъекцию конструктора в приведенном выше примере, мы получаем:

public class MyLogger : ILogger // Others will depend on our logger.
{
    private readonly TextWriter _output;
    private readonly IClock _clock;

    // Dependencies are injected through the constructor
    public MyLogger(TextWriter stream, IClock clock)
    {
        _output = stream;
        _clock = clock;
    }

    public void LogRecord(string somethingToLog)
    {
        // We can now use our dependencies through the abstraction 
        // and without knowledge of the lifespans of the dependencies
        _output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
    }
}

(Требуется конкретный Clock, который, конечно, может вернуться к DateTime.Now, и две зависимости должны быть предоставлены контейнером IoC через инжекцию конструктора)

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

[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
    // Arrange
    var mockClock = new Mock<IClock>();
    mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
    var fakeConsole = new StringWriter();

    // Act
    new MyLogger(fakeConsole, mockClock.Object)
        .LogRecord("Foo");

    // Assert
    Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}

Следующие шаги

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

  • отображение между каждой абстракцией и сконфигурированной конкретной реализацией (например, "каждый раз, когда потребитель запрашивает IBar, возвращает ConcreteBar экземпляр" )
  • политики могут быть установлены для управления продолжительностью жизни каждой зависимости, например создать новый объект для каждого экземпляра-потребителя, совместно использовать экземпляр-одиночку для всех потребителей, использовать один и тот же экземпляр-зависимость только для одного потока и т. д.
  • В .Net контейнеры IoC знают о таких протоколах, как IDisposable, и принимают на себя ответственность Disposing зависимостей в соответствии с настроенным управлением продолжительностью жизни.

Как правило, после настройки / загрузки контейнеров IoC они работают в фоновом режиме, позволяя кодировщику сосредоточиться на имеющемся коде, а не беспокоиться о зависимостях.

Ключ к DI-дружественному коду заключается в том, чтобы избежать статической связи классов и не использовать new () для создания зависимостей

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

Но преимуществ много, особенно в способности тщательно проверить свой класс интересов.

Примечание : Создание / отображение / проекция (через new ..()) POCO / POJO / Сериализация DTO / Графы сущностей / Анонимные проекции JSON и др., Т.е. классы или записи «Только данные» - используются или возвращенные из методов не рассматриваются как зависимости (в смысле UML) и не подлежат DI. Использование new для проецирования это просто замечательно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...