Куда направлены зависимости в чистой архитектуре?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Куда направлены зависимости в чистой архитектуре?
Это один из ключевых принципов 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) всегда чистое, независимое и может жить без внешних деталей.