Как правильно реализовать принцип внедрения зависимостей из SOLID?
Что проверяют
Различаете ли вы три понятия, которые часто смешивают: принцип инверсии зависимостей (буква D в SOLID), внедрение зависимостей как технику и DI-контейнер как инструмент. Следом обычно спрашивают, как вы подменяете зависимости в тестах и нужен ли в Python контейнер вообще.
Как отвечать
Сразу поправьте формулировку: в SOLID буква D означает Dependency Inversion Principle. Модули верхнего уровня не зависят от модулей нижнего, оба зависят от абстракции, и абстракцию определяет потребитель. Внедрение зависимостей это способ этого добиться: объект не создаёт то, что ему нужно, а получает снаружи.
Как это выглядит в Python:
- абстракция:
typing.Protocolдля структурной типизации: наследоваться не нужно, достаточно совпасть по методам.abc.ABCуместен, когда нужна проверка при создании экземпляра или общая реализация; - внедрение через конструктор:
def __init__(self, repo: UserRepository), без созданияPostgresUserRepository()внутри; - корень композиции: зависимости собираются в одном месте: в
main, фабрике приложения или настройке фреймворка; - FastAPI:
Depends()строит граф зависимостей на запрос, а в тестахapp.dependency_overridesподменяет их без патчей; - контейнеры:
dependency-injector,punq,injector. В небольшом сервисе хватает ручной сборки, контейнер оправдан, когда графов много и есть разные области жизни.
Главный практический выигрыш виден в тестах: вы передаёте фейковый репозиторий в конструктор вместо unittest.mock.patch("app.services.db.PostgresRepo"). Тест перестаёт зависеть от путей импорта и не ломается при переносе модуля.
Где спотыкаются
Называют DI и DIP одним и тем же. Можно внедрять конкретный класс через конструктор и при этом зависеть от деталей, принцип тогда не соблюдён.
Прячут зависимость в значении по умолчанию: repo=PostgresRepo() в сигнатуре. Объект создаётся один раз при определении функции и становится общим для всех вызовов, а тесты снова требуют патчей.
Переусложняют: ABC с префиксом I на каждый класс с единственной реализацией и контейнер в скрипте на двести строк. Абстракцию стоит вводить там, где реально есть вторая реализация или граница модуля.