Перейти к основному содержимому

Принцип инверсии зависимостей (DIP)

Разработчику Архитектору Инженеру

Принцип D — Dependency Inversion

Что такое Dependency Inversion?

Это пятый принцип SOLID. Его часто путают с DI, но это разные вещи.

Dependency Inversion Principle (DIP) гласит:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Play ITЗагрузка интерактивного демо…


Как работает DI?

Представим, что у нас есть модуль А (бизнес-логика, "важная штука") и модуль Б (база данных, "техническая деталь").

КЛАСС СервисПользователей
поле база := новый Postgres() // ❌ верхний уровень знает деталь

метод Зарегистрировать(имя)
база.Сохранить(имя)
конец
КОНЕЦ

Справочно на Python (нарушение DIP)

Код ITЗагрузка примера кода…

Здесь возникает проблема:

  • модуль А знает, что есть PostgresDB;
  • если мы захотим заменить СУБД на MySQL, то полезем внутрь UserService;
  • если PostgresDB поменяет имя метода с save на store, то сломается модуль А.

Модуль А (важный) зависит от модуля Б (деталь). Это нарушение DIP.

Для соблюдения добавляем абстракцию (интерфейс) - она не зависит ни от А, ни от Б.

ИНТЕРФЕЙС БазаДанных
метод Сохранить(данные)
КОНЕЦ

КЛАСС Postgres РЕАЛИЗУЕТ БазаДанных …
КЛАСС MySql РЕАЛИЗУЕТ БазаДанных …

КЛАСС СервисПользователей
поле база: БазаДанных
конструктор(база)
метод Зарегистрировать(имя)
база.Сохранить(имя)
конец
КОНЕЦ

Справочно на Python

Код ITЗагрузка примера кода…

Что изменилось:

  • модуль А больше не знает про PostgreDB или MySQLDB. Он знает только про DatabaseInterface;
  • модуль Б (PostgresDB) знает про DatabaseInterface и реализует его;
  • направление зависимости развернулось - раньше АБ, теперь оба А и Б → абстракция.

Было:

UserService (важный) --------> PostgresDB (деталь)
(зависит)

Стало (DIP):

DatabaseInterface (абстракция)
↑ ↑
| |
| |
UserService (важный) PostgresDB (деталь)
(зависит от абстракции) (зависит от абстракции)

Модуль А (важный) не должен знать про модуль Б (деталь).

Они оба должны знать про интерфейс, который А использует, а Б реализует.

Как этого добиться:

  1. Создаём интерфейс (в Python — абстрактный класс с методами).
  2. Модуль А принимает в конструктор любой объект, у которого есть этот интерфейс.
  3. Модуль Б реализует этот интерфейс.

Всё. Вот и весь смысл.


Пример

Представим, что у нас есть лампочка (LightBulb), которую можно включить и выключить. И у нас есть переключатель (Switch), который может управлять лампочкой. Если мы будем создавать класс Switch, то он должен установить связь с LightBulb.

Код ITЗагрузка примера кода…

Но при таком подходе (это кстати обычная связь без DIP), Switch зависит от конкретной реализации Lightbulb, и нельзя подключить другие девайсы, пока прямо их не перечислим. И если применить DIP, устанавливая задачу, чтобы Switch не зависел от конкретного вида устройство, а был более универсальным:

Код ITЗагрузка примера кода…

Здесь можно увидеть, что добавляется интерфейс Switchable (переключаемый). Класс Lighbulb наследует Switchable, и Fan тоже Switchable - словом, как лампочка, так и вентилятор - оба "переключаемые" и теоретически, к ним можно применять класс. А класс Swich изменил подход, создав себе device, у которого тип данных - Switchable.

Класс Switch (переключатель) получает поле device, которое потом используется в методе Switch, что сделало его универсальным - класс больше не зависит от конкретики, и стал более гибким, расширяемым и тестируем.

Смысл? А теперь нам не нужно будет создавать Fan и прочие устройства со своими экземплярами в Switch. Теперь мы просто при вызове конструктора Switch(Switchable device) будем передавать любой из Switchable.