[feat] add guides
This commit is contained in:
parent
1fbf77f879
commit
c5d5e243c1
7 changed files with 836 additions and 43 deletions
227
docs/CLEAN_ARCHITECTURE_RU.md
Normal file
227
docs/CLEAN_ARCHITECTURE_RU.md
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# Чистая архитектура, SOLID и контракты
|
||||
|
||||
## Что такое чистая архитектура
|
||||
|
||||
Чистая архитектура - это способ строить проект слоями так, чтобы бизнес-правила жили отдельно от фреймворков, транспорта, базы данных и внешних SDK.
|
||||
|
||||
Главная идея:
|
||||
|
||||
- самые важные правила лежат ближе к центру
|
||||
- внешние детали зависят от внутренних правил
|
||||
- смена FastAPI, базы или OTel не должна ломать `domain/` и `usecase/`
|
||||
|
||||
В этом проекте центр выглядит так:
|
||||
|
||||
- `domain/` - сущности и доменные ошибки
|
||||
- `usecase/` - сценарии приложения и интерфейсы
|
||||
- `repository/` - реализации хранилищ
|
||||
- `adapter/` - HTTP, конфиг, DI, observability
|
||||
|
||||
## SOLID коротко
|
||||
|
||||
### S - Single Responsibility Principle
|
||||
|
||||
Один класс делает одну работу.
|
||||
|
||||
Хорошо:
|
||||
|
||||
- `UserService` только исполняет сценарий CRUD пользователя
|
||||
- `InMemoryUserRepository` только хранит и читает пользователей
|
||||
- `StdoutLogger` только пишет логи в stdout
|
||||
|
||||
Плохо:
|
||||
|
||||
- use case читает env
|
||||
- router сам работает с базой
|
||||
- repository знает про HTTP-статусы
|
||||
|
||||
### O - Open/Closed Principle
|
||||
|
||||
Код должен расширяться без переписывания ядра.
|
||||
|
||||
Пример: можно добавить `PostgresUserRepository`, не меняя `GetUser`, если обе реализации соблюдают один и тот же контракт `UserRepository`.
|
||||
|
||||
### L - Liskov Substitution Principle
|
||||
|
||||
Любая реализация интерфейса должна корректно подменять другую.
|
||||
|
||||
Если use case ожидает `UserRepository`, ему должно быть все равно, это in-memory репозиторий, PostgreSQL или HTTP client repository.
|
||||
|
||||
### I - Interface Segregation Principle
|
||||
|
||||
Интерфейсы должны быть небольшими и точными.
|
||||
|
||||
Лучше так:
|
||||
|
||||
```py
|
||||
class UserRepository(Protocol):
|
||||
def get(self, user_id: str) -> User | None: ...
|
||||
def get_by_email(self, email: str) -> User | None: ...
|
||||
def save(self, user: User) -> None: ...
|
||||
```
|
||||
|
||||
Чем так:
|
||||
|
||||
```py
|
||||
class MegaRepository(Protocol):
|
||||
def get_user(self, user_id: str) -> User | None: ...
|
||||
def save_user(self, user: User) -> None: ...
|
||||
def send_email(self, message: str) -> None: ...
|
||||
def collect_metric(self, name: str) -> None: ...
|
||||
```
|
||||
|
||||
### D - Dependency Inversion Principle
|
||||
|
||||
Высокоуровневый код не зависит от низкоуровневых деталей.
|
||||
|
||||
Use case не должен зависеть от конкретного `InMemoryUserRepository`, `FastAPI`, `OTLPMetricExporter` или `logging.Logger`.
|
||||
Он зависит только от абстракций.
|
||||
|
||||
## Как работает инверсия зависимостей
|
||||
|
||||
Смысл DIP здесь такой:
|
||||
|
||||
1. use case объявляет, что ему нужно
|
||||
2. это описывается через `Protocol`
|
||||
3. внешняя реализация удовлетворяет протоколу
|
||||
4. composition root связывает use case с реализацией
|
||||
|
||||
Пример:
|
||||
|
||||
```py
|
||||
class UserRepository(Protocol):
|
||||
def get(self, user_id: str) -> User | None: ...
|
||||
|
||||
|
||||
class GetUser:
|
||||
def __init__(self, repository: UserRepository, logger: Logger, tracer: Tracer) -> None:
|
||||
self._repository = repository
|
||||
self._logger = logger
|
||||
self._tracer = tracer
|
||||
```
|
||||
|
||||
`GetUser` знает только о контракте `UserRepository`.
|
||||
Реализация уже подключается снаружи, в `adapter/di/container.py`.
|
||||
|
||||
## Что такое Protocol в Python
|
||||
|
||||
`Protocol` - это способ описать интерфейс через набор методов без жесткого наследования.
|
||||
|
||||
Если класс имеет нужные методы, он уже подходит под протокол.
|
||||
|
||||
Это удобно для чистой архитектуры, потому что:
|
||||
|
||||
- use case задает контракт рядом с местом использования
|
||||
- внешние слои могут реализовать контракт без сильной связки
|
||||
- тестировать проще
|
||||
|
||||
Пример логгера:
|
||||
|
||||
```py
|
||||
class Logger(Protocol):
|
||||
def info(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||
def warning(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||
def error(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||
```
|
||||
|
||||
Пример SRP через протоколы:
|
||||
|
||||
- `Logger` отвечает только за логирование
|
||||
- `Metrics` только за счетчики и гистограммы
|
||||
- `Tracer` только за спаны
|
||||
|
||||
## Архитектура слоев в проекте
|
||||
|
||||
### `domain/`
|
||||
|
||||
Здесь лежат самые базовые вещи:
|
||||
|
||||
- сущности
|
||||
- value objects, если появятся
|
||||
- доменные ошибки
|
||||
|
||||
Пример:
|
||||
|
||||
- `domain/user.py` - сущность `User`
|
||||
- `domain/error.py` - `DomainError`, `UserNotFoundError`, `UserConflictError`
|
||||
|
||||
`domain/` не должен знать ни про FastAPI, ни про OTel, ни про YAML, ни про env.
|
||||
|
||||
### `usecase/`
|
||||
|
||||
Здесь лежат прикладные сценарии.
|
||||
|
||||
Use case:
|
||||
|
||||
- принимает простые входные данные
|
||||
- работает через интерфейсы
|
||||
- возвращает доменные сущности или прикладные результаты
|
||||
- не знает про HTTP и инфраструктуру
|
||||
|
||||
Пример:
|
||||
|
||||
- `usecase/user.py` -> `GetUser`
|
||||
- `usecase/interface.py` -> `UserRepository`, `Logger`, `Metrics`, `Tracer`
|
||||
|
||||
### `repository/`
|
||||
|
||||
Здесь живут реализации контрактов доступа к данным.
|
||||
|
||||
Пример из проекта:
|
||||
|
||||
```py
|
||||
class InMemoryUserRepository(UserRepository):
|
||||
def __init__(self, tracer: Tracer, users: Iterable[User] | None = None) -> None:
|
||||
self._users = {user.id: user for user in users or ()}
|
||||
self._tracer = tracer
|
||||
```
|
||||
|
||||
Что важно:
|
||||
|
||||
- repository может зависеть от `domain/` и `usecase/`
|
||||
- repository не должен тянуть HTTP детали в use case
|
||||
- repository может быть заменен на PostgreSQL, Redis, gRPC client, REST client
|
||||
|
||||
Еще примеры возможных repository:
|
||||
|
||||
- `PostgresUserRepository`
|
||||
- `RedisSessionRepository`
|
||||
- `ExternalBillingRepository`
|
||||
|
||||
### `adapter/`
|
||||
|
||||
Это внешний слой.
|
||||
|
||||
Здесь находятся:
|
||||
|
||||
- FastAPI router и схемы
|
||||
- загрузка конфига
|
||||
- DI контейнер
|
||||
- логирование, метрики, трейсы
|
||||
- интеграции со сторонними библиотеками
|
||||
|
||||
Adapter переводит внешний мир в язык use case и обратно.
|
||||
|
||||
## Пример разделения ответственности
|
||||
|
||||
Хорошая цепочка:
|
||||
|
||||
1. HTTP handler получает `user_id`
|
||||
2. handler создает `GetUserQuery`
|
||||
3. use case вызывает `UserRepository`
|
||||
4. repository возвращает `User`
|
||||
5. handler маппит `User` в HTTP response model
|
||||
|
||||
Плохая цепочка:
|
||||
|
||||
1. router сам идет в базу
|
||||
2. router сам открывает span
|
||||
3. router сам собирает config
|
||||
4. router сам решает бизнес-правила
|
||||
|
||||
## Почему это удобно
|
||||
|
||||
- проще тестировать use case отдельно от HTTP и OTel
|
||||
- проще менять инфраструктуру
|
||||
- проще читать проект по слоям
|
||||
- проще давать задачу AI-агенту без риска смешать ответственности
|
||||
Loading…
Add table
Add a link
Reference in a new issue