Принцип инверсии зависимостей (DIP)
Принцип D: Dependency Inversion
Что такое Dependency Inversion?
Это пятый принцип SOLID. Его часто путают с DI, но это разные вещи.
Dependency Inversion Principle (DIP) гласит:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Как работает 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 (деталь)
(зависит от абстракции) (зависит от абстракции)
Модуль А (важный) не должен знать про модуль Б (деталь).
Они оба должны знать про интерфейс, который А использует, а Б реализует.
Как этого добиться:
- Создаём интерфейс (в Python — абстрактный класс с методами).
Модуль Апринимает в конструктор любой объект, у которого есть этот интерфейс.Модуль Бреализует этот интерфейс.
Всё. Вот и весь смысл.
Пример
Представим, что у нас есть лампочка (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.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Класс User имеет ссылку на UserProfile как на внутреннее поле — это агрегация и прямая зависимость. Dependency Inversion - это принцип проектирования, а Dependency Injection - паттерн проектирования. DIP говорит что делать, DI - как делать. Управление зависимостями — это фундаментальная дисциплина в проектировании и разработке программного обеспечения. Зависимости возникают естественным образом при взаимодействии компонентов — классов,… Итоги и вопросы по теме Чек-лист самопроверки для самопроверки в энциклопедии Вселенная IT.Управление зависимостями в программных проектах
Внедрение зависимостей (Dependency Injection)
Итоги
Чек-лист самопроверки