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

Куда направлены зависимости в чистой архитектуре?

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

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

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

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

Куда направлены зависимости в чистой архитектуре?

Это один из ключевых принципов Clean Architecture. Ответ: все зависимости направлены ВНУТРЬ, к центру (domain слой).

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

┌─────────────────────────────────────────┐
│        PRESENTATION (UI, REST API)      │
│  (Controllers, Views, API Handlers)     │
│         ↓ (зависит от)                  │
├─────────────────────────────────────────┤
│      APPLICATION (Use Cases)            │
│  (Services, Interactors, Orchestration) │
│         ↓ (зависит от)                  │
├─────────────────────────────────────────┤
│        DOMAIN (Entities, Rules)         │
│  (Business Logic, Models, Interfaces)   │
│         ← (НЕ зависит от кого-либо!)    │
└─────────────────────────────────────────┘

Главное правило: Domain слой не должен зависеть ни от чего выше!

Почему именно так?

1. Domain слой — ядро системы

Domain содержит бизнес-правила, которые не должны зависеть от:

  • Деталей реализации базы данных
  • Фреймворков (Django, FastAPI, Flask)
  • Способа представления (REST, GraphQL, gRPC)
  • Способа хранения (SQL, MongoDB, файлы)
# ПРАВИЛЬНО — Domain НЕ зависит
from domain.models import User
from domain.interfaces import UserRepository

class User:
    """Сущность, определяет правила бизнеса"""
    def __init__(self, email: str, password: str):
        if not email or "@" not in email:
            raise ValueError("Invalid email")  # Бизнес-правило
        self.email = email
        self.password = password

class UserRepository(Protocol):
    """Интерфейс, НЕ зависит от БД"""
    def save(self, user: User) -> None: ...
    def find_by_email(self, email: str) -> User: ...
# НЕПРАВИЛЬНО — Domain зависит от деталей
from django.db import models  # ❌ Framework зависимость!

class User(models.Model):  # ❌ Привязано к Django!
    email = models.EmailField()
    password = models.CharField(max_length=255)
    
    class Meta:
        db_table = 'users'  # ❌ БД деталь!

2. Application слой — логика сценариев

Application использует Domain entities и interfaces.

# application/use_cases.py
from domain.models import User
from domain.interfaces import UserRepository
from domain.exceptions import UserAlreadyExists

class RegisterUserUseCase:
    def __init__(self, repository: UserRepository):
        self.repository = repository  # Зависит от интерфейса (Domain)
    
    def execute(self, email: str, password: str) -> User:
        # Проверка бизнес-правила
        existing = self.repository.find_by_email(email)
        if existing:
            raise UserAlreadyExists()
        
        # Создание entity
        user = User(email, password)
        
        # Сохранение через interface
        self.repository.save(user)
        
        return user

Application НЕ знает, как реально сохраняется user (SQL? NoSQL? File?)

3. Presentation слой — интерфейс пользователя

Presentation зависит от Application.

# presentation/api/routes.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from application.use_cases import RegisterUserUseCase
from domain.exceptions import UserAlreadyExists
from infrastructure.repositories import PostgresUserRepository

router = APIRouter()

class RegisterRequest(BaseModel):
    email: str
    password: str

@router.post("/register")
async def register(request: RegisterRequest):
    try:
        repository = PostgresUserRepository()  # Конкретная реализация
        use_case = RegisterUserUseCase(repository)
        user = use_case.execute(request.email, request.password)
        return {"id": user.id, "email": user.email}
    except UserAlreadyExists:
        raise HTTPException(status_code=409, detail="User exists")

Presentation:

  • Зависит от Application (use_case)
  • Зависит от Infrastructure (repository)
  • НЕ должен содержать бизнес-логику

4. Infrastructure слой — детали реализации

Infrastructure реализует интерфейсы Domain.

# infrastructure/repositories.py
from domain.models import User
from domain.interfaces import UserRepository
import psycopg2  # ❌ БД зависимость НА ПЕРИФЕРИИ

class PostgresUserRepository(UserRepository):
    def __init__(self):
        self.conn = psycopg2.connect(...)  # Детали БД
    
    def save(self, user: User) -> None:
        query = "INSERT INTO users (email, password) VALUES (%s, %s)"
        self.conn.execute(query, (user.email, user.password))
        self.conn.commit()
    
    def find_by_email(self, email: str) -> User:
        query = "SELECT * FROM users WHERE email = %s"
        result = self.conn.execute(query, (email,)).fetchone()
        if not result:
            return None
        return User(result['email'], result['password'])

Ключ: Infrastructure реализует интерфейсы Domain, НЕ наоборот!

Полная архитектура

domain/                          ← НЕ зависит от кого-либо
├── models.py                   ← User, Post, Comment
├── exceptions.py               ← UserNotFound, InvalidEmail
└── interfaces.py               ← UserRepository, EmailService

application/                     ← Зависит от domain
├── use_cases.py                ← RegisterUser, LoginUser
├── dto.py                      ← Request/Response models
└── services.py                 ← Сценарии

infrastructure/                  ← Зависит от domain
├── repositories/               ← PostgresUserRepository
├── services/                   ← SMTPEmailService
└── config.py                   ← Конфигурация БД

presentation/                    ← Зависит от application
├── api/                        ← REST endpoints
├── controllers/                ← Обработчики запросов
└── schemas.py                  ← JSON schemas

Пример: наоборот (ПЛОХО)

# ❌ НЕПРАВИЛЬНО — зависимости идут НАРУЖУ

# domain/models.py
from django.db import models  # ❌ Зависит от Framework!
from fastapi import HTTPException  # ❌ Зависит от API Framework!

class User(models.Model):
    email = models.EmailField()
    
    def register(self):
        if User.objects.filter(email=self.email).exists():
            raise HTTPException(status_code=409)  # ❌ API детали в Domain!

Проблемы:

  • Domain связан с Django и FastAPI
  • Нельзя переключиться на другой фреймворк
  • Сложно тестировать (нужны mock всех dependencies)
  • Нарушена инверсия управления (Inversion of Control)

Инверсия управления (IoC) и зависимостей

Зависимости управляются через интерфейсы, а не конкретные классы.

# ✅ ПРАВИЛЬНО — IoC через интерфейсы

from typing import Protocol
from domain.models import User

# Domain определяет интерфейс
class UserRepository(Protocol):
    def save(self, user: User) -> None: ...

# Application использует интерфейс
class RegisterUserUseCase:
    def __init__(self, repo: UserRepository):
        self.repo = repo  # Зависит от интерфейса, не конкретного класса

# Infrastructure реализует интерфейс
class PostgresUserRepository:
    def save(self, user: User) -> None:
        # Конкретная реализация
        pass

# Presentation собирает всё вместе
repo = PostgresUserRepository()  # Можно легко заменить!
use_case = RegisterUserUseCase(repo)

Преимущества такой архитектуры

1. Независимость от Framework:

# Domain работает везде
my_user = User(email="john@example.com", password="123")

# И с Django
from infrastructure.repositories import DjangoUserRepository
use_case1 = RegisterUserUseCase(DjangoUserRepository())

# И с FastAPI
from infrastructure.repositories import FastAPIUserRepository
use_case2 = RegisterUserUseCase(FastAPIUserRepository())

# И в тестах
from tests.mocks import MockUserRepository
use_case3 = RegisterUserUseCase(MockUserRepository())

2. Легко тестировать:

from unittest.mock import Mock

def test_register_user():
    mock_repo = Mock(spec=UserRepository)
    mock_repo.find_by_email.return_value = None
    
    use_case = RegisterUserUseCase(mock_repo)
    user = use_case.execute("john@example.com", "password")
    
    assert user.email == "john@example.com"
    mock_repo.save.assert_called_once()

3. Легко менять детали реализации:

# Были на PostgreSQL
repo = PostgresUserRepository()

# Хотим на MongoDB — просто меняем класс
repo = MongoUserRepository()  # Интерфейс тот же!

use_case = RegisterUserUseCase(repo)  # Код не меняется!

4. Бизнес-правила защищены: Domain слой содержит все правила и не подвержен изменениям интерфейсов.

Диаграмма зависимостей

PRESENTATION
    ↑ (зависит от)
APPLICATION
    ↑ (зависит от)
DOMAIN
    ↑ (НЕ зависит)
    |
    └─ Application НЕ может зависеть от Domain напрямую
    └─ Infrastructure НЕ может зависеть от Presentation

Итоговое правило

Правило единственного направления (The Dependency Rule):

Внешние слои зависят от внутренних слоёв, но не наоборот.

Овальная архитектура:

          ↙─ Presentation
       ↙──── Application
    Domain ←─ Infrastructure
     ↖────→ (через интерфейсы!)

Это гарантирует, что ядро (Domain) всегда чистое, независимое и может жить без внешних деталей.

Куда направлены зависимости в чистой архитектуре? | PrepBro