7.4 KiB
7.4 KiB
Как эта архитектура реализована в проекте
Общая схема
Проект собирается так:
adapter/config/loader.pyчитает YAML,.envи env varsadapter/di/container.pyстроит единый контейнерadapter/observability/factory.pyсоздает logger, metrics и tracer runtime- repository и usecase создаются один раз при старте
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
Рабочая последовательность:
- Если нужна новая бизнес-сущность или ошибка, добавить ее в
domain/ - Если use case зависит от внешнего ресурса, описать контракт в
usecase/ - Создать класс use case
- Внедрить зависимости через конструктор
- Подключить 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 ручку
Последовательность:
- Создать или переиспользовать use case
- При необходимости добавить схемы в
adapter/http/fastapi/schemas.py - Добавить dependency, если нужен новый use case из контейнера
- Добавить маршрут в
adapter/http/fastapi/routers/v1/router.py - Преобразовать HTTP input в команду или query use case
- Преобразовать результат use case в response model
- Смаппить доменные ошибки в
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_TOKENAPP_SIGNING_KEY
Переопределение через env
Можно переопределять YAML через переменные окружения, например:
APP_HTTP_HOSTAPP_HTTP_PORTAPP_LOGGING_LEVELAPP_LOGGING_OUTPUTAPP_LOGGING_FORMATAPP_LOGGING_FILE_PATHAPP_METRICS_ENABLEDAPP_TRACING_ENABLEDAPP_OTEL_LOGS_ENDPOINTAPP_OTEL_METRICS_ENDPOINTAPP_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