← Назад к вопросам

Где будешь внедрять зависимости на примере Onion архитектуры в Python?

3.0 Senior🔥 151 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Внедрение зависимостей в Onion архитектуре

В Onion архитектуре внедрение зависимостей строится на принципе инверсии управления (IoC) и следует направлению зависимостей от внешних слоёв к внутренним. Давайте разберёмся, где и как это делается.

Структура слоёв

Onion архитектура состоит из слоёв (от центра к внешним):

  1. Domain (ядро) — бизнес-логика, интерфейсы
  2. Application — use cases, сценарии использования
  3. Infrastructure — реализация интерфейсов (БД, API, кэш)
  4. Presentation — контроллеры, HTTP endpoints, CLI

Где внедрять зависимости

1. На уровне Application (Use Cases)

Это правильное место для DI. Use Case должен зависеть от интерфейсов (abstract), которые определены в Domain слое.

from abc import ABC, abstractmethod
from dataclasses import dataclass

# Domain layer — интерфейсы
class UserRepository(ABC):
    @abstractmethod
    def get_by_id(self, user_id: str) -> dict:
        pass
    
    @abstractmethod
    def save(self, user: dict) -> None:
        pass


class EmailService(ABC):
    @abstractmethod
    def send(self, email: str, message: str) -> None:
        pass


# Application layer — Use Case
@dataclass
class UpdateUserUseCase:
    user_repo: UserRepository
    email_service: EmailService
    
    def execute(self, user_id: str, new_email: str) -> None:
        user = self.user_repo.get_by_id(user_id)
        user["email"] = new_email
        self.user_repo.save(user)
        self.email_service.send(new_email, "Email updated")

2. На уровне Infrastructure (реализация)

Инфраструктурный слой реализует интерфейсы из Domain.

# Infrastructure layer
class PostgresUserRepository(UserRepository):
    def __init__(self, connection_string: str):
        self.db = create_connection(connection_string)
    
    def get_by_id(self, user_id: str) -> dict:
        # SQL запрос
        return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
    
    def save(self, user: dict) -> None:
        # UPDATE запрос
        self.db.execute(f"UPDATE users SET email = {user[email]} WHERE id = {user[id]}")


class SmtpEmailService(EmailService):
    def __init__(self, smtp_host: str, smtp_port: int):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
    
    def send(self, email: str, message: str) -> None:
        # Отправка письма
        pass

3. На уровне Presentation (сборка контейнера)

Презентационный слой собирает объекты и внедряет зависимости.

# Presentation layer — HTTP handler / CLI
from fastapi import FastAPI
from dependency_injector import containers, providers

# Контейнер зависимостей
class Container(containers.DeclarativeContainer):
    # Конфигурация
    config = providers.Configuration()
    
    # Infrastructure
    user_repo = providers.Singleton(
        PostgresUserRepository,
        connection_string=config.db_connection_string
    )
    
    email_service = providers.Singleton(
        SmtpEmailService,
        smtp_host=config.smtp_host,
        smtp_port=config.smtp_port
    )
    
    # Application
    update_user_use_case = providers.Factory(
        UpdateUserUseCase,
        user_repo=user_repo,
        email_service=email_service
    )


app = FastAPI()
container = Container()
container.config.from_env()


@app.post("/users/{user_id}/email")
async def update_user_email(user_id: str, new_email: str):
    use_case = container.update_user_use_case()
    use_case.execute(user_id, new_email)
    return {"status": "updated"}

Направление зависимостей

Критически важно: зависимости направлены ВНУТРЬ

Presentation → Application → Infrastructure
                    ↓
              Domain (интерфейсы)
  • Domain не знает о реализациях
  • Application зависит от Domain интерфейсов, не от конкретных реализаций
  • Infrastructure реализует Domain интерфейсы
  • Presentation зависит от всех слоёв (собирает контейнер)

Пример без DI контейнера

Можно внедрять вручную, если проект маленький:

def main():
    # Создаём реализации
    user_repo = PostgresUserRepository("postgres://...")
    email_service = SmtpEmailService("smtp.gmail.com", 587)
    
    # Внедряем в use case
    use_case = UpdateUserUseCase(user_repo, email_service)
    
    # Используем
    use_case.execute("user_123", "new@email.com")

Ключевые правила

Делай так:

  • Интерфейсы в Domain слое
  • Реализации в Infrastructure
  • Use Cases внедряют зависимости через конструктор
  • Presentation собирает контейнер

Не делай так:

  • Не импортируй Infrastructure классы в Domain
  • Не создавай глобальные синглтоны в бизнес-логике
  • Не передавай зависимости через параметры функций (передавай через конструктор)

Этот подход обеспечивает тестируемость (легко мокировать), гибкость (легко менять реализацию) и чистоту архитектуры.