Недавно меня спросили на собеседовании: “В чём разница между Dependency injection и Dependency inversion principle из SOLID”. Я знал определение каждого из них, но провести четкую границу между этими понятиями у меня не получилось. В этой статье я хочу кратко описать различие между понятиями Dependency inversion principle (DIP), Inversion of control (IoC) и Dependency injection (DI) на примере чистой архитектуры и Android фреймворка. Итак, поехали.
Определение
В первом определении фигурирует понятие “модуль”. Это очень важное понятие без которого понять DIP не получится.
Чтобы не было недопонимания, рассмотрим определения на примере. Допустим, нам надо получить список прочитанных книг и вывести его пользователю. Для этого будем использовать классы BooksInteractor и BooksRepository. Каждый из этих классов положим в свой модуль BI и BR соответственно. Модуль класса BooksInteractor зависит от класса BooksRepository.
BooksInteractor’y нужно получить список книг из BooksRepository. Интерактору совершенно не важно, как репозиторий получит эти данные, а значит в данном случае BooksInteractor является абстракцией для BooksRepository. Согласно второму определению BooksInteractor (абстракиция) НЕ должен зависеть от BooksRepository (реализация). А вот репозиторий наоборот должен знать о модуле BI. Что ж это получается: интерактор должен лежать внутри репозитория? Нет, чтобы инвертировать зависимости, нам достаточно покрыть BooksRepository интерфейсом IBooksRepository, а этот интерфейс положить в модуль класса BooksInteractor. Теперь вернемся к 1-ому определению DIP и взглянем на диаграмму.
Смотрите-ка, модули верхних уровней (модуль BI) не зависят от модулей нижних уровней (модуль BR). А оба модуля зависят от абстракции (от интерфейса IBooksRepository). Если вы уловили магию инвертирования зависимости с помощью покрытия репозитория интерфейсом, вы поняли принцип инверсии зависимостей. Поздравляю! Самое сложное вы поняли. Подробнее почитать про DIP можете в этой статье на хабре.
Мы изучили понятие «принцип инверсии зависимостей». Теперь перейдем к ещё одной инверсии — инверсии контроля. Само понятие очень обширно и в программировании может означать одно из трех:
Dependency injection (внедрение зависимостей) — это механизм передачи классу его зависимостей. С этим вы всегда встречаетесь, когда нужно передать зависимость в другой класс. Существует несколько способов внедрения зависимостей: через конструктор (Constructor Injection), через метод (Method Injection) и через свойство (Property Injection). Каждый из этих методов используется для своих целей. Но тут важно понять, что Dependency Injection — это всего лишь передача зависимости в конструктор, метод или свойство
Узнать отличия DIP, DI и IoC от других авторов можно тут и тут.
Буду рад вашим комментариям и отзывам!
Dependency inversion principle
Определение
A. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
В первом определении фигурирует понятие “модуль”. Это очень важное понятие без которого понять DIP не получится.
модуль — логически взаимосвязанная совокупность функциональных элементов
Чтобы не было недопонимания, рассмотрим определения на примере. Допустим, нам надо получить список прочитанных книг и вывести его пользователю. Для этого будем использовать классы BooksInteractor и BooksRepository. Каждый из этих классов положим в свой модуль BI и BR соответственно. Модуль класса BooksInteractor зависит от класса BooksRepository.
BooksInteractor’y нужно получить список книг из BooksRepository. Интерактору совершенно не важно, как репозиторий получит эти данные, а значит в данном случае BooksInteractor является абстракцией для BooksRepository. Согласно второму определению BooksInteractor (абстракиция) НЕ должен зависеть от BooksRepository (реализация). А вот репозиторий наоборот должен знать о модуле BI. Что ж это получается: интерактор должен лежать внутри репозитория? Нет, чтобы инвертировать зависимости, нам достаточно покрыть BooksRepository интерфейсом IBooksRepository, а этот интерфейс положить в модуль класса BooksInteractor. Теперь вернемся к 1-ому определению DIP и взглянем на диаграмму.
Смотрите-ка, модули верхних уровней (модуль BI) не зависят от модулей нижних уровней (модуль BR). А оба модуля зависят от абстракции (от интерфейса IBooksRepository). Если вы уловили магию инвертирования зависимости с помощью покрытия репозитория интерфейсом, вы поняли принцип инверсии зависимостей. Поздравляю! Самое сложное вы поняли. Подробнее почитать про DIP можете в этой статье на хабре.
Inversion of Control
Мы изучили понятие «принцип инверсии зависимостей». Теперь перейдем к ещё одной инверсии — инверсии контроля. Само понятие очень обширно и в программировании может означать одно из трех:
- Инверсия интерфейсов — делегирование взаимоотношения между модулями посреднику-интерфейсу. Где это применяется? Например, в DIP, который мы изучили ранее.
- Инверсия создания — делегирование создания и связывания зависимостей посреднику (фабрики, DI/IOC контейнеры).
- Инверсия процесса — делегирование контроля над своей программой фреймворку, который сам управляет последовательностью вызовов функций. Первое, что приходит на ум в качестве примера, — Android фреймворк. Ты расширяешь класс Activity и Fragment, отдавая контроль над их жизненным циклом фреймворку.
Dependency Injection
Dependency injection (внедрение зависимостей) — это механизм передачи классу его зависимостей. С этим вы всегда встречаетесь, когда нужно передать зависимость в другой класс. Существует несколько способов внедрения зависимостей: через конструктор (Constructor Injection), через метод (Method Injection) и через свойство (Property Injection). Каждый из этих методов используется для своих целей. Но тут важно понять, что Dependency Injection — это всего лишь передача зависимости в конструктор, метод или свойство
Рассматриваем edge cases
- Может ли быть DI без IoC и DIP? Да, может. Создаем экземпляр конкретного класса A и передаем его в объект класса B через конструктор, метод или свойство.
- Может ли быть DIP без IoC? Нет, DIP является одним из способов реализации Инверсии интерфейсов в IoC.
- Может ли быть DIP без DI? Да, мы можем завязать нижние модули на интерфейс. Классы верхнего модуля будут работать с интерфейсом-абстракцией, но конкретная реализация класса нижнего модуля будет создаваться в конструкторе верхнего модуля.
Узнать отличия DIP, DI и IoC от других авторов можно тут и тут.
Буду рад вашим комментариям и отзывам!
lair
А откуда вы это взяли? Я не видел такого определения. А если его выкинуть, то и дальше все меняется:
… если в IoC нет никакой "инверсии интерфейсов", то и DIP не является способом реализации IoC.
Другое дело, что считаете ли вы передачу объекта, реализующего интерфейс, его потребителю IoC? Я не считаю. Поэтому в моем мире возможен DIP без IoC.
infinity_coder Автор
Я встречал подобное определение в этой статье и не только. Конкретно в этой статье все эти 3 определения уже красиво были сформулированы и они как раз описывали все то, что говорилось в других 8-10 статьях про IoC. Если подходить к термину IoС с самых истоков и разобраться что оно на самом деле значит, то и DIP, и колбэки — всё это инверсия контроля.
Если закрыть глаза на одно из определений IoC, то DIP действительно возможен без IoC.
Но если допустить, что определение «Инверсии интерфейсов» возможно, то можно рассмотреть следующий кейс. Мы покрыли BooksRepository интерфейсом и теперь BooksRepository какой-то «независимый класс». Никто не вызывает его методы напрямую (как и методы ЖЦ у Activity или Fragment). Единственый, кто контроллирует BooksRepository — это кто-то извне через интерфейс. А значит, что благодаря DIP у нас реализуется и IoC. Но DIP != IoC.
Если объект покрыт интерфейсом и передается в конструктор — это ещё не DIP и не IoC. DIP — это не покрытие всех классов интерфейсами, а скорее способ инвертирования зависимостей для обеспечения слабой межмодульной связности.
Несмотря на глубокое погружение в данную тему, я могу все ещё заблуждаться, поэтому буду рад, если поправите, где я не прав.
lair
Это какой-то неизвестный мне человек, который не приводит никаких источников.
А здесь этого определения нет.
Совершенно не обязательно. Пока вы не процитируете эти самые "истоки", это не более чем ваш домысел.
Что такое "независимый класс"?
Это невозможно, потому что кто-то должен его создавать.
Нет, не значит. То, что кто-то вызывает объект через интерфейс, не означает, что у вас реализован IoC.
Почему это? Определение DIP: "One should "depend upon abstractions, [not] concretions.". Вы передаете абстракцию, код-пользователь знает только об абстракции — DIP выполнен.
infinity_coder Автор
— «Суть которого в том, что каждый компонент системы должен быть как можно более изолированным от других, не полагаясь в своей работе на детали конкретной реализации других компонентов.»
— «Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.»
— «делегирование взаимоотношения между модулями посреднику-интерфейсу»
Каждое из этих понятий сильно похоже на описание DIP.
Я соглашусь, что последнее определение не описывает DIP, но разве это не то самое, что мы достигаем с помощью DIP?
Все верно, кто-то создает. А кто-то другой управляет объектом через интерфейс.
Определение DIP состоит из 2-х постулатов. Первое из которых должно также выполняться.
lair
"Похоже на описание DIP" — возможно. Но мало ли что на что похоже? Вопрос в том, где же взять определение IoC, чтобы разговор был предметным.
Во-первых, это вообще не определение. Во-вторых, для меня интерфейс-посредник — это именно посредник, а не интерфейс одного из модулей. И это — проблема определений, снова.
Тот, кто объект создает, контролирует его явно.
Ну так он [первый постулат] и выполняется.