Как и где происходит внедрение зависимостей (DI) в луковой архитектуре (Onion Architecture) на Python?

Ответ

В луковой архитектуре внедрение зависимостей (Dependency Injection, DI) происходит на границах слоев и следует Принципу инверсии зависимостей (Dependency Inversion Principle). Внутренние слои не зависят от внешних; вместо этого все слои зависят от абстракций.

Механизм DI

  1. Определение Абстракции (Порт): Во внутреннем слое (например, application или domain) определяется абстрактный интерфейс. В Python это обычно abc.ABC или typing.Protocol.
  2. Реализация (Адаптер): Во внешнем, инфраструктурном слое (infrastructure) создается конкретный класс, который реализует этот интерфейс (например, для работы с базой данных или внешним API).
  3. Внедрение: Зависимость (конкретная реализация) передается в конструктор класса внутреннего слоя. Это позволяет внутреннему слою работать с абстракцией, не зная о деталях реализации.
  4. Сборка (Composition Root): В самой внешней точке приложения (например, в main.py или с помощью DI-контейнера) создаются экземпляры конкретных реализаций и внедряются в сервисы.

Пример на Python

from abc import ABC, abstractmethod

# === Domain Layer: Сущность ===
class User:
    def __init__(self, name: str):
        self.name = name

# === Application Layer: Абстракция (Порт) ===
class UserRepository(ABC):
    @abstractmethod
    def get_user(self, user_id: int) -> User:
        ...

# === Infrastructure Layer: Конкретная реализация (Адаптер) ===
class PostgresUserRepository(UserRepository):
    def get_user(self, user_id: int) -> User:
        # Логика для получения пользователя из PostgreSQL
        print(f"Fetching user {user_id} from PostgreSQL...")
        return User(name="John Doe")

# === Application Layer: Сервис, использующий абстракцию ===
class UserService:
    # Зависимость внедряется через конструктор
    def __init__(self, user_repo: UserRepository):
        self._user_repo = user_repo

    def process_user(self, user_id: int):
        user = self._user_repo.get_user(user_id)
        print(f"Processing user: {user.name}")

# === Composition Root: Точка сборки приложения ===
if __name__ == "__main__":
    # Создаем конкретную реализацию
    db_repository = PostgresUserRepository()

    # Внедряем ее в сервис
    user_service = UserService(user_repo=db_repository)

    # Используем сервис
    user_service.process_user(123)

Ответ 18+ 🔞

Да ты посмотри, какой тут у нас замечательный подход, блядь! Луковая архитектура, сука, не просто так придумана. Тут вся суть — в этом самом внедрении зависимостей, которое, как хитрая жопа, происходит на стыке слоёв. И главный принцип — инверсия, ёпта! Не внутренние слои пляшут под дудку внешних, а наоборот, все дружно зависят от каких-то абстракций, от интерфейсов. Красота, в рот меня чих-пых!

Как это, блядь, работает

  1. Выдумываем абстракцию (Порт, блядь): Где-то в самом нутре, в domain или application, рисуем на бумажке интерфейс. В Питоне это либо abc.ABC, либо typing.Protocol. Просто говорим: «Вот такая-то штука должна уметь делать то-то». А как она это сделает — нам похуй, это проблемы внешнего мира.
  2. Делаем реализацию (Адаптер, сука): А вот уже где-то снаружи, в этом своём infrastructure, какой-нибудь ушлый инженер пишет конкретный класс. Этот класс берёт и говорит: «О, я умею делать то, что ты просил!» — и реализует тот самый интерфейс. Может, лезет в PostgreSQL, может, в MongoDB, а может, вообще API какого-нибудь сервиса дергает.
  3. Внедряем эту хрень: Потом мы берём эту готовую реализацию и, как просроченную колбасу, засовываем её в конструктор какого-нибудь сервиса из внутреннего слоя. Сервис-то наш умный, он работает только с абстракцией, с интерфейсом. Ему по барабану, что ему подсунули — он знает, какие методы вызывать.
  4. Собираем всё в кучу (Composition Root): И вот, в самом начале, когда приложение только просыпается (в main.py, например), мы как раз и делаем эту подмену. Создаём конкретную реализацию и запихиваем её туда, где ждут абстракцию. Можно, конечно, и DI-контейнер использовать, но это уже для особо одарённых.

Пример, чтобы совсем пиздец всё стало ясно

from abc import ABC, abstractmethod

# === Domain Layer: Сущность ===
class User:
    def __init__(self, name: str):
        self.name = name

# === Application Layer: Абстракция (Порт) ===
class UserRepository(ABC):
    @abstractmethod
    def get_user(self, user_id: int) -> User:
        ...

# === Infrastructure Layer: Конкретная реализация (Адаптер) ===
class PostgresUserRepository(UserRepository):
    def get_user(self, user_id: int) -> User:
        # Логика для получения пользователя из PostgreSQL
        print(f"Fetching user {user_id} from PostgreSQL...")
        return User(name="John Doe")

# === Application Layer: Сервис, использующий абстракцию ===
class UserService:
    # Зависимость внедряется через конструктор
    def __init__(self, user_repo: UserRepository):
        self._user_repo = user_repo

    def process_user(self, user_id: int):
        user = self._user_repo.get_user(user_id)
        print(f"Processing user: {user.name}")

# === Composition Root: Точка сборки приложения ===
if __name__ == "__main__":
    # Создаем конкретную реализацию
    db_repository = PostgresUserRepository()

    # Внедряем ее в сервис
    user_service = UserService(user_repo=db_repository)

    # Используем сервис
    user_service.process_user(123)

Вот и вся магия, ёпта! UserService нихуя не знает про PostgreSQL. Он знает только, что у него есть некий user_repo, у которого можно вызвать get_user. А уж откуда тот пользователя достанет — из базы, из файла или из своей буйной фантазии — это уже не его собачье дело. Главное — принцип соблюдён, зависимости инвертированы, и жить стало проще. Ну, в теории, по крайней мере.