четверг, 27 ноября 2014 г.

DI vs. DIP vs. IoC

Существует три схожих понятия, связанных передачей и управлением зависимостями, в каждом из которых есть слово “инверсия” (inversion) или “зависимость” (dependency):

  • IoC – Inversion of Control (Инверсия управления)
  • DI – Dependency Injection (Внедрение зависимостей)
  • DIP – Dependency Inversion Principle (Принцип инверсии зависимостей)

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

Inversion of Control (IoC)

Инверсия управления (IoC, Inversion of Control) – это достаточно общее понятие, которое отличает библиотеку от фреймворка. Классическая модель подразумевает, что вызывающий код контролирует внешнее окружение и время и порядок вызова библиотечных методов. Однако в случае фреймворка обязанности меняются местами: фреймворк предоставляет некоторые точки расширения, через которые он вызывает определенные методы пользовательского кода.

Простой метод обратного вызова или любая другая форма паттерна Наблюдатель является примером инверсии. Зная значение понятия IoC становится ясно, что такое понятие как IoC-контейнер лишено смысла, если только данный «контейнер» не предназначен для упрощения создания фрейморков.

ch051_Image1

Dependency Injection (DI)

Внедрение зависимостей (DI, Dependency Injection) – это механизм передачи классу его зависимостей. Существует несколько конкретных видов или паттернов внедрения зависимостей: внедрение зависимости через конструктор (Constructor Injection), через метод (Method Injection) и через свойство (Property Injection).

class ReportProcessor
{
   
private readonly IReportSender _reportSender
;

   
// Constuctor Injection: передача обязательной зависимости
    public ReportProcessor(IReportSender reportSender
)
    {
       
_reportSender = reportSender
;
       
Logger = LogManager.DefaultLogger
;
    }

   
// Method Injection: передача обязательных зависимостей метода
    public void SendReport(Report report, IReportFormatter formatter
)
    {
       
Logger.Info("Sending report..."
);
       
var formattedReport = formatter.Format(report
);
       
_reportSender.SendReport(formattedReport
);
       
Logger.Info("Report has been sent"
);
    }

   
// Property Injection: установка необязательных "инфраструктурных" зависимостей
    public ILogger Logger { get; set; }
}

Разные виды внедрения зависимостей предназначены для решения определенных задач. Через конструктор передаются обязательные зависимости класса, без которых работа класса невозможна (IReportSender - обязательная зависимость класса ReportProcessor). Через метод передаются зависимости, которые нужны лишь одному методу, а не всем методам класса (IReportFormatter необходим только методу отправки отчета, а не классу ReportProcessor целиком). Через свойства должны устанавливаться лишь необязательные зависимости (обычно, инфраструктурные), для которых существует значение по умолчанию (свойство Logger содержит разумное значение по умолчанию, но может быть заменено позднее).

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

Dependency Inversion Principle (DIP)

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

class ReportProcessor
{
   
private readonly ISocket _socket
;
   
public ReportProcessor(ISocket socket
)
    {
       
_socket = socket
;
    }
   
public void SendReport(Report report, IStringBuilder stringBuilder
)
    {
       
stringBuilder.AppendFormat(CreateHeader(report
));
       
stringBuilder.AppendFormat(CreateBody(report
));
       
stringBuilder.AppendFormat(CreateFooter(report
));
       
_socket.Connect
();
       
_socket.Send(ConvertToByteArray(stringBuilder
));
    }


}

Класс ReportProcessor все еще принимает «абстракцию» в аргументах конструктора - ISocket, но эта «абстракция» находится на несколько уровней ниже уровня формирования и отправки отчетов. Аналогично дела обстоят и с аргументом метода SendReport: «абстракция» IStringBuilder не соответствует принципу инверсии зависимостей, поскольку оперирует более низкоуровневыми понятиями, чем требуется. На этом уровне нужно оперировать не строками, а отчетами.

В результате, в данном примере используется внедрение зависимостей (DI), но данный код не следует принципу инверсии зависимостями (DIP).

Подведем итоги.

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

Дополнительные ссылки

25 комментариев:

  1. Серж, хороший пост. Корректный )))
    Вот бы на английском - отослал бы ребятам пилящим MSR Orleans.
    У них с этим реальные проблемы. Собственно как и у практически всех академиков. Инжинииринг - это не к ним ))))

    ОтветитьУдалить
    Ответы
    1. Там по ссылкам есть и англоязычные варианты. В них букв поболее будет, но идея там такая же.

      Погодь, а эти MSR Orleans - они же в МС-е? А ты мне пришли свои комменты, а я этим товарищам в личку постучу их их вежливо передам!

      Удалить
  2. Три серьезных темы изложил в 3 строки, класс!
    > Зависимости класса должны располагаться на текущем или более высоком уровне абстракции
    Скорее, более низком?

    ОтветитьУдалить
    Ответы
    1. Зависимости должны быть на текущем или более высоком уровне абстракции.

      Зависимости низкого уровня вредны. Это как если класс Reporter-а будет принимать ISocket. Сокет - это абстракция очень низкого уровня. Поэтому репортер должен зависеть от чего-то более высокоуровневого, например, ICommunicationProxy или что-то такого, а не зависеть от низкоурвевых абстракций, типа сокетов.

      Удалить
  3. Ответил здесь: http://sparethought.wordpress.com/2014/11/28/re-di-vs-dip-vs-ioc/

    ОтветитьУдалить
    Ответы
    1. Ответил там же, продублирую здесь:

      Я не проводил параллель между IoC и фреймворком, а привел это в качестве одного из примеров. В статье явно говорится, что Наблюдатель является одним из примеров инверсии управления. Очевидно, что наблюдатель не имеет никакого отношения ни к фреймворкам/библиотекам.

      Удалить
  4. Ты быстрее написал пост, чем я прочитал твою ссылку на аглийскую статью. Но теперь всё стало на свои места. Спасибо!
    PS: к своему стыду скажу, что я раньше не знал, что буква "I" расшифровывается по разному в DI и DIP.

    ОтветитьУдалить
    Ответы
    1. Ну так даже лучше, значит можно проревьюить понятность материала:)

      Удалить
  5. Спасибо, Сергей. Вещи на полках сознания наконец-то нашли свои места и перестали дрейфовать с полки на полку.

    ОтветитьУдалить
  6. Спасибо за статью, теперь всё встало на свои места в голове!

    ОтветитьУдалить
  7. Сергей, первая картинка в статье не загружается.

    ОтветитьУдалить
  8. Сергей, тогда возникает вопрос определения данных терминов в вики. Если исходить из вашего материала по фаулеру, то в вики полный бред и подмена понятий.
    Тогда возникает вопрос что такое IoC контейнер? И что такое DI контейнер и в чем их принципиальные различия?
    ps в том же spring употребляют имеено понятие IoC контейнера, который служит для управления зависимостями. Хотя в определении инверсии управления нет ни слова о зависимостях...

    ОтветитьУдалить
    Ответы
    1. Конечно, подмена понятий. Фаулер напакостил в свое время. Под IoC контейнером можете понимать фреймворк. Под DI контейнером просто контейнер - концентратор зависимостей
      Если в вашей системе все компоненты имеют свои зависимости, то где-то в системе какой-то класс или фабрика должны знать, что внедрять во все эти компоненты. Вот что делает DI-контейнер.

      Удалить
  9. Сергей, тогда возникает вопрос определения данных терминов в вики. Если исходить из вашего материала по фаулеру, то в вики полный бред и подмена понятий.
    Тогда возникает вопрос что такое IoC контейнер? И что такое DI контейнер и в чем их принципиальные различия?
    ps в том же spring употребляют имеено понятие IoC контейнера, который служит для управления зависимостями. Хотя в определении инверсии управления нет ни слова о зависимостях...

    ОтветитьУдалить
    Ответы
    1. Игорь, а о какой Вики идёт речь (rus/eng)? Я бегло глянул IOC в англоязычной Вики и ереси там не увидел....

      Удалить
    2. Этот комментарий был удален автором.

      Удалить
    3. Я про русскую вики. Вот цитата:
      "Одной из реализаций IoC является внедрение зависимостей (англ. dependency injection)."
      Но ведь это полный абсурд. Как внедрение зависимостей может быть реализацией инверсии управления, когда эта два несвязанных понятия.
      Как я понял, под IoC контейнером понимается сам фреймворк.
      Тогда в чем отличие IoC контейнера от DI контейнера?

      Удалить
    4. Интересно, что на английской вики понятие IOC Container не упоминается.

      Причина, почему этот термин может использоваться, исключительно историческое.

      Более корректное название для всех Unity, Sprint-ов и прочих autofac-ов - это именно DI-контейнер, хотя в некоторых случаях, что-то типа castle-а могут применяться для реализации декоратора, что можно отнести и к полноценному IoC.

      Просто в начале нулевых, когда начался бум на это дело, IoC и DI были синонимами, что довольно прочно укоренилось в головах. Отсюда и путаница.

      Статья Фаулера, как и эта статья, это как раз попытка вернуть понятия под контроль, поскольку очень важно, чтобы разные люди вкладывали в одно и то же слово, одно и то же значение (ну или хотя бы приблизительно одно и то же).

      Удалить
  10. Я тоже не могу вкурить, почему Dependency Injection считается реализацией Inversion of Control. Если этому просто историческая причина, почему даже в англоязычной Википедии, которая должна быть, по идее, актуальна и точна, не расставлено всё по своим местам? Напротив, сказано: "Dependency injection is a specific type of IoC using contextualized lookup." (и ссылка на Фаулера).

    Но ведь вернее было бы сказать, что Dependency Injection — это просто один из удобных (и правильных) способов предоставления внешних зависимостей тем частям кода, которые пишет пользователь фреймворка или системы, построенной по принципу Inversion of Control.

    Верно ли это? Или я что-то не понимаю?

    ОтветитьУдалить
    Ответы
    1. Как я понимаю, более простыми словами, DI относится к композиции ваших классов, т.е. с помощью DI вы "комопозите" классы в коде. IoC это как runtime enviroment of your code, например, Spring Framework является IoC

      Удалить
    2. Следует учитывать, то IoC контейнер (фреймворк) и принцип IoC - это разные вещи. DI -это действительно одна из реализаций принципа IoC, так что тут Вики не ошибается. Есть, конечно, и другие его реализации; например, паттерн Observer. При этом внутри любых фреймворков, DI используется часто и широко, в этом Вы правы.

      Удалить
    3. Да, историческая причина. Так придумал Фаулер. Хотя DI служит и для IoC и для DIP Фаулер заострял внимание только на IoC когда вводил этот термин. https://martinfowler.com/articles/injection.html#InversionOfControl

      Это добавляет путаницы, конечно

      Удалить
  11. Я бы еще добавил:
    IoC может быть реализован
    а) без дополнительных сущностей - через DI или Service Locator
    б) с дополнительными сущностями через шаблон наблюдатель, через фреймворки, через события и реакцию на них

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

    ОтветитьУдалить