записан подход к проектированию: чистая архитектура и тактические шаблоны DDD
- architecture.md: раздел «Слои и модель домена» — таблица слоёв с колонкой «чего не знает», шаблоны со своим сегодняшним предметом в коде, запрет анемичной модели - CLAUDE.md: раздел «Проектирование» с подходом и ссылкой на раздел архитектуры - названо, что чистоту домена не держит ни одно правило archrules
This commit is contained in:
+53
-2
@@ -77,9 +77,11 @@
|
||||
задача возвращается в работу, нормирует
|
||||
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||
- **Чистая архитектура.** Зависимости направлены внутрь, к домену: внутренний
|
||||
слой не знает внешнего никогда. `internal/service` знает только
|
||||
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в точке входа
|
||||
`cmd/transcriber`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||
`cmd/transcriber`. Слои, их дома и словарь модели — раздел «Слои и модель
|
||||
домена» ниже. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||
они же держат обратные направления: транспорты не знают друг о друге, адаптер
|
||||
не знает ни ядра, ни транспортов.
|
||||
*Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http`
|
||||
@@ -87,6 +89,55 @@
|
||||
роутер этого хранилища, а не наш сервер поверх него. Правила на это
|
||||
направление нет намеренно.
|
||||
|
||||
## Слои и модель домена
|
||||
|
||||
**Подход — чистая архитектура.** Зависимость идёт только внутрь: домен не знает
|
||||
ни хранилища, ни транспорта, а знание о внешнем мире живёт интерфейсом в портах
|
||||
и реализацией в инфраструктуре.
|
||||
|
||||
| Слой | Дом | Что живёт | Чего не знает |
|
||||
| --- | --- | --- | --- |
|
||||
| Домен | `internal/entity` | сущности, объекты-значения, доменные события, инварианты значениями | ничего, кроме стандартной библиотеки и единой точки времени `internal/clock` |
|
||||
| Порты | `internal/contract` | интерфейсы репозиториев и внешних служб, типизированные ошибки | реализаций |
|
||||
| Прикладной слой | `internal/service` | шаги конвейера: порядок, повтор, приговор | адаптеров и входов |
|
||||
| Инфраструктура | `internal/adapter` | репозитории, ffmpeg, Yandex, шаги схемы | ядра и входов |
|
||||
| Входы | `internal/controller` | HTTP и пул воркеров | друг друга |
|
||||
| Сборка | `cmd/transcriber` | подстановка реализаций в порты, подъём сервера и пула | — |
|
||||
|
||||
Направления держат тесты-сканеры `internal/archrules` — все, кроме чистоты
|
||||
самого домена. **Её не держит ничто**: правила смотрят ядро, входы и адаптеры, а
|
||||
импорт внешней библиотеки в `internal/entity` сегодня пройдёт молча.
|
||||
|
||||
**Модель домена ведётся тактическими шаблонами DDD.** Шаблон называется здесь
|
||||
вместе со своим сегодняшним предметом — перечень растёт вместе с моделью:
|
||||
|
||||
- **Сущность** — `entity.AudioRecord`: у неё идентичность и поведение
|
||||
(`MoveToState`, `Halt`, `Resume`, `Postpone`), а не набор полей при сервисе;
|
||||
- **корень агрегата** — она же: файлы, тексты, структура, попытки распознавания и
|
||||
журнал событий принадлежат записи и живут её идентификатором, а правит агрегат
|
||||
держатель захвата;
|
||||
- **объект-значение** — `entity.Stage` со своими сроками, `entity.StuckLimits`,
|
||||
`entity.Replica`, `entity.RecognitionResult`: сравниваются по значению и своей
|
||||
идентичности не имеют;
|
||||
- **доменное событие** — `entity.RecordEvent`: что случилось с записью, чьей
|
||||
рукой и чем кончилось;
|
||||
- **репозиторий** — интерфейсы `internal/contract`, реализации под
|
||||
`internal/adapter/repo`;
|
||||
- **служба домена** — правило, не принадлежащее одной сущности, живёт функцией
|
||||
пакета домена (`entity.WorkingStages`, `entity.SanitizeOriginalFilename`);
|
||||
- **фабрика** — `entity.NewInProgressResult` и соседи: значение приходит
|
||||
согласованным, а не заполняется полями снаружи.
|
||||
|
||||
**Анемичной модели не заводим.** Новое поведение записи ищет дом сначала в
|
||||
домене; прикладной слой назначает порядок шагов, а не правила. Признак нарушения
|
||||
наблюдаем: правило о записи, записанное в `internal/service` условием над её
|
||||
полями, принадлежит `internal/entity`.
|
||||
|
||||
Место, где подход нарушен сегодня, названо изъятием в «Принципах»: транспорт
|
||||
знает адаптер хранилища, потому что HTTP-поверхность и есть роутер этого
|
||||
хранилища. Изъятие снимает задача `storage-without-pocketbase` — своя отдача
|
||||
файла и свои маршруты возвращают транспорту независимость от инфраструктуры.
|
||||
|
||||
## Компоненты
|
||||
|
||||
Каждый — строкой со ссылкой на capability, а не пересказом её требований.
|
||||
|
||||
Reference in New Issue
Block a user