Зависеть от абстракций
Принцип инверсии зависимостей гласит: высокоуровневый код — ваша бизнес-логика — не должен зависеть от низкоуровневых деталей, например от конкретной базы данных или формата файла. И тот и другой должны зависеть от абстракции: в Python это протокол.
«Инверсия» — это направление стрелки. Вместо того чтобы сервис указывал на одно конкретное хранилище, сервис указывает на протокол — и каждое хранилище указывает на тот же протокол с другой стороны.
Жёсткая привязка к деталям
Этот сервис пользователей сам создаёт и использует конкретный DiskStore. Он работает — но сервис намертво привязан к одному хранилищу. Другое хранилище, более быстрое хранилище, поддельное хранилище для тестов: каждое из них означает правку самого сервиса.
Сервис, намертво привязанный к своему хранилищу
UserService сам создаёт внутри себя свой DiskStore. Высокоуровневая логика регистрации теперь зависит от низкоуровневой детали — файла на диске.
Вывод: полная программа на Python
class DiskStore:
def save(self, user):
print("запись", user, "в файл users.txt на диске")
# UserService сам создаёт своё хранилище — намертво привязан к детали.
class UserService:
def __init__(self):
self.store = DiskStore()
def register(self, user):
print("регистрация пользователя:", user)
self.store.save(user)
service = UserService()
service.register("ana")
Инвертируем зависимость
Решение: сервис запрашивает протокол Storage в своём конструкторе и никогда не узнаёт, какое именно хранилище он получил. Хранилище на диске и хранилище в памяти оба реализуют этот протокол.
Теперь стрелки с обеих сторон указывают на абстракцию — и замена хранилища сводится к изменению одной строки там, где создаётся сервис, а не внутри него.
Сервис принимает протокол
Та же логика регистрации, два разных хранилища. UserService(DiskStore()) или UserService(MemoryStore()) — сам код сервиса никогда не меняется.
Вывод: полная программа на Python
from typing import Protocol
# Абстракция, от которой зависят обе стороны.
class Storage(Protocol):
def save(self, user: str) -> None: ...
class DiskStore:
def save(self, user: str) -> None:
print("запись", user, "в файл users.txt на диске")
class MemoryStore:
def save(self, user: str) -> None:
print("хранение", user, "в памяти")
class UserService:
def __init__(self, store: Storage):
self.store = store
def register(self, user: str) -> None:
print("регистрация пользователя:", user)
self.store.save(user)
on_disk = UserService(DiskStore())
on_disk.register("ana")
in_memory = UserService(MemoryStore())
in_memory.register("ben")
Выигрыш: тестирование без диска
Поскольку сервис зависит от абстракции, тест может передать ему хранилище в памяти и сразу проверить результат — без файлов и без уборки после себя. Именно инверсия зависимостей делает код простым для модульного тестирования.
Вывод: полная программа на Python
from typing import Protocol
class Storage(Protocol):
def save(self, user: str) -> None: ...
# MemoryStore запоминает, что было сохранено, — идеально для проверок.
class MemoryStore:
def __init__(self):
self.users: list[str] = []
def save(self, user: str) -> None:
self.users.append(user)
class UserService:
def __init__(self, store: Storage):
self.store = store
def register(self, user: str) -> None:
self.store.save(user)
store = MemoryStore()
service = UserService(store)
service.register("ana")
service.register("ben")
if len(store.users) == 2:
print("проверка пройдена: сохранено 2 пользователя")
print("сохранённые пользователи:", store.users)