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

7.4 KiB
Raw Permalink Blame History

Как эта архитектура реализована в проекте

Общая схема

Проект собирается так:

  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/
  3. Создать класс use case
  4. Внедрить зависимости через конструктор
  5. Подключить use case в adapter/di/container.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, который его использует.

class UserRepository(Protocol):
    def get(self, user_id: str) -> User | None: ...
    def save(self, user: User) -> None: ...

2. Потом сделать реализацию

Реализация уходит во внешний слой, например в repository/.

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

Шаблон:

@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

Локальный запуск

APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run

Запуск с OTel

APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run-otel

Docker Compose

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