master/docs/PROJECT_GUIDE_RU.md
2026-03-26 21:56:10 +03:00

216 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Как эта архитектура реализована в проекте
## Общая схема
Проект собирается так:
1. `adapter/config/loader.py` читает YAML, `.env` и env vars
2. `adapter/di/container.py` строит единый контейнер
3. `adapter/observability/factory.py` создает logger, metrics и tracer runtime
4. repository и usecase создаются один раз при старте
5. `adapter/http/fastapi/app.py` создает FastAPI app и подключает router + middleware
Это и есть composition root: все внешние зависимости собираются в одном месте и потом передаются внутрь.
## Как здесь реализована чистая архитектура
### `domain/`
- хранит сущности и доменные ошибки
- не зависит от framework и SDK
### `usecase/`
- описывает сценарии приложения
- задает интерфейсы через `Protocol`
- работает только с абстракциями
### `repository/`
- реализует интерфейсы доступа к данным
- может использовать tracer, clients, DB drivers
### `adapter/`
- оборачивает внешние инструменты
- переводит HTTP/config/OTel в контракты приложения
## Как создать новый use case
Рабочая последовательность:
1. Если нужна новая бизнес-сущность или ошибка, добавить ее в `domain/`
2. Если use case зависит от внешнего ресурса, описать контракт в `usecase/`
4. Создать класс use case
5. Внедрить зависимости через конструктор
6. Подключить use case в `adapter/di/container.py`
Минимальный шаблон:
```py
from usecase.interface import Logger, Metrics, Tracer, UserRepository
from domain.user import User
class CreateUser:
def __init__(self, repository: UserRepository, logger: Logger, metrics: Metrics, tracer: Tracer) -> None:
self._repository = repository
self._logger = logger
self._metrics = metrics
self._tracer = tracer
def execute(self, email: str) -> User:
with self._tracer.start_span('usecase.create_user', attrs={'user.email': email}):
user = self._repository.save(email)
self._metrics.increment('user.create.total')
self._logger.info('user_create', attrs={'user.email': email})
return user
```
Правила:
- не импортировать FastAPI в use case
- не читать env vars в use case
- не работать с `Request`, `Response`, `HTTPException`
## Как работать с repository
### 1. Сначала описать интерфейс
Контракт должен жить рядом с use case, который его использует.
```py
class UserRepository(Protocol):
def get(self, user_id: str) -> User | None: ...
def save(self, user: User) -> None: ...
```
### 2. Потом сделать реализацию
Реализация уходит во внешний слой, например в `repository/`.
```py
class InMemoryUserRepository(UserRepository):
def __init__(self, tracer: Tracer) -> None:
self._tracer = tracer
self._users: dict[str, User] = {}
```
### 3. Подключить в контейнере
Именно `adapter/di/container.py` решает, какую реализацию дать use case.
Это важно: use case не должен сам выбирать repository.
## Как добавить новую HTTP ручку
Последовательность:
1. Создать или переиспользовать use case
2. При необходимости добавить схемы в `adapter/http/fastapi/schemas.py`
3. Добавить dependency, если нужен новый use case из контейнера
4. Добавить маршрут в `adapter/http/fastapi/routers/v1/router.py`
5. Преобразовать HTTP input в команду или query use case
6. Преобразовать результат use case в response model
7. Смаппить доменные ошибки в `HTTPException`
Шаблон:
```py
@router.post('/users', response_model=UserResponse)
def create_user(payload: CreateUserRequest, usecase: CreateUser = Depends(get_create_user)) -> UserResponse:
try:
user = usecase.execute(payload.email)
except UserConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return UserResponse(id=user.id, email=user.email)
```
Правило: router остается тонким.
## Как пользоваться логгером, метриками и трейсами
### Логгер
- использовать для бизнес-событий и предупреждений
- передавать данные через `attrs`
- сообщения делать короткими и стабильными
### Метрики
- использовать для счетчиков и длительностей
- не создавать условные ветки `if metrics_enabled`
- runtime сам подставит рабочую или noop-реализацию
### Трейсы
- открывать span на границе важной операции
- класть ключевые атрибуты в `attrs`
- ошибки писать через `record_error`, если это делает смысл явным
## Как конфигурировать проект
### Базовая конфигурация
Основной файл - `config/app.yaml`.
Там лежат:
- имя приложения
- окружение
- HTTP host/port
- режим логирования
- флаги метрик и трейсов
- OTel endpoints
- имя заголовка токена
### Секреты
Обязательные секреты берутся из env:
- `APP_API_TOKEN`
- `APP_SIGNING_KEY`
### Переопределение через env
Можно переопределять YAML через переменные окружения, например:
- `APP_HTTP_HOST`
- `APP_HTTP_PORT`
- `APP_LOGGING_LEVEL`
- `APP_LOGGING_OUTPUT`
- `APP_LOGGING_FORMAT`
- `APP_LOGGING_FILE_PATH`
- `APP_METRICS_ENABLED`
- `APP_TRACING_ENABLED`
- `APP_OTEL_LOGS_ENDPOINT`
- `APP_OTEL_METRICS_ENDPOINT`
- `APP_OTEL_TRACES_ENDPOINT`
### Локальный запуск
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run
```
### Запуск с OTel
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run-otel
```
### Docker Compose
```bash
APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make compose-up
```
## Короткая памятка
- новая бизнес-логика -> `usecase/`
- новый контракт -> рядом с use case
- новая реализация контракта -> `repository/` или внешний `adapter/`
- новый HTTP маршрут -> `adapter/http/fastapi/`
- новый infra/runtime code -> `adapter/`
- зависимости связывать только в `adapter/di/container.py`