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

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

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

Принцип D: Dependency Inversion

Что такое Dependency Inversion?

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

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

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

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

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

# Модуль Б (нижний уровень) - деталь
class PostgresDB:
def save(self, Данные):
print(f"Сохраняю {Данные} в PostgreSQL")

# Модуль А (верхний уровень) - важная логика
class UserService:
def __init__(self):
self.db = PostgresDB() # ❌ Жёсткая привязка к конкретной БД

def register(self, name):
self.db.save(name)

# Пользуемся
service = UserService()
service.register("Алексей")

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

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

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

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

# Абстракция (интерфейс) - то, от чего зависят оба
class DatabaseInterface:
def save(self, Данные):
raise NotImplementedError

# Модуль Б (деталь) зависит от абстракции
class PostgresDB(DatabaseInterface):
def save(self, Данные):
print(f"Сохраняю {Данные} в PostgreSQL")

class MySQLDB(DatabaseInterface):
def save(self, Данные):
print(f"Сохраняю {Данные} в MySQL")

# Модуль А (бизнес-логика) тоже зависит от абстракции
class UserService:
def __init__(self, db: DatabaseInterface): # ✅ получает абстракцию извне
self.db = db

def register(self, name):
self.db.save(name)

# Пользуемся
postgres = PostgresDB()
service = UserService(postgres) # можно отдать любую БД
service.register("Алексей")

mysql = MySQLDB()
service2 = UserService(mysql) # тот же код, другая БД
service2.register("Мария")

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

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

Было:

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

Стало (DIP):

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

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

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

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

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

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


Пример

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

class LightBulb {
public void turnOn() { ... }
}

class Switch {
private LightBulb bulb = new LightBulb(); // жёсткая зависимость
}

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

interface Switchable {
void turnOn();
void turnOff();
}

class LightBulb implements Switchable { ... }
class Fan implements Switchable { ... }

class Switch {
private Switchable device; // зависит от абстракции
public Switch(Switchable device) {
this.device = device;
}
}

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

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

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).