[feat] add guides
This commit is contained in:
parent
1fbf77f879
commit
c5d5e243c1
7 changed files with 836 additions and 43 deletions
216
docs/PROJECT_GUIDE_RU.md
Normal file
216
docs/PROJECT_GUIDE_RU.md
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# Как эта архитектура реализована в проекте
|
||||
|
||||
## Общая схема
|
||||
|
||||
Проект собирается так:
|
||||
|
||||
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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue