Domain-Driven Design
Использовал технологию DDD для реализации доменной backend-логики.
Реализовывал:
- отдельные слои
app/Application,app/Domain,app/Infrastructure; - use-case handlers вместо логики в контроллерах;
- value objects и domain-entities;
- порты и инфраструктурные адаптеры для БД, очередей и файловых хранилищ;
- отдельные read/write сценарии для API-операций.
Пример из практики #1
Дневной рацион пользователя
Данный пример описывает логику вокруг дневного питания пользователя: формирование рациона на конкретную дату, добавление и изменение отдельных приёмов пищи, пересчёт порций, хранение состояния рациона и подготовку данных для дальнейшего nutrition-расчёта и подбора блюд.
DailyRationAggregate-Aggregate Root, который представляет рацион пользователя на конкретный день и задаёт границу согласованности для всех изменений внутри этого рациона;MealEntry-Entityвнутри агрегата, отдельный приём пищи с собственным состоянием: тип, рецепт, порция и привязка к дате;MealDate-Value Object, который представляет дату рациона как доменное значение, а не просто строку из запроса;MealType- доменныйenum, который ограничивает допустимые типы приёмов пищи и убирает "магические строки";Portion-Value Objectдля размера порции с проверкой допустимого диапазона;NutritionVector-Value Objectдля набора нутриентов, который используется в расчётах калорий, белков, жиров и углеводов;UserNutritionProfile-Value Objectс параметрами пользователя, нужными для расчёта nutrition goal;MealSelectionOptions-Value Objectс параметрами и ограничениями для алгоритма подбора блюда.
Описание:
- рацион на день описан как aggregate;
- отдельный приём пищи оформлен как entity со своим состоянием;
- дата, порция и nutrition-данные оформлены как value objects с явными правилами;
- доменные ограничения и инварианты живут ближе к самой модели, а не в контроллере.
Подбор и пересчёт состава приёмов пищи
Использовал отдельные domain services:
MealOptimizer;BreakfastOptimizer;LunchOptimizer;DinnerOptimizer;SnackOptimizer;DailyNutritionGoalCalculator.
Реализовал функциональность:
- распределение дневной nutrition goal по типам приёмов пищи;
- подбор базового рецепта и supplement-компонента;
- расчёт portion-ов;
- сравнение вариантов по score/error;
- работа с дефицитами по макроэлементам.
Прикладной алгоритм подбора рецептов для рациона вынесен в доменный слой, а не спрятан в SQL, контроллере или util-классах.
Пример из практики #2
Export jobs для PDF
Данный пример описывает логику вокруг асинхронного экспорта дневного рациона в PDF: создание export job, постановку задачи в очередь, хранение текущего статуса, сохранение результата и последующую выдачу файла пользователю.
| Объект | Роль в DDD | Для чего нужен |
|---|---|---|
ExportJob | Entity | Представляет export job как отдельную доменную сущность со своим lifecycle: создание, обработка, успешное завершение или ошибка. |
ExportJobId | Value Object | Хранит идентификатор export job как отдельное доменное значение, а не как произвольную строку. |
ExportJobPayload | Value Object | Содержит данные, с которыми запускается экспорт, например дату рациона и служебные параметры запроса. |
ExportJobStatus | Value Object / enum | Описывает текущее состояние export job и ограничивает допустимые переходы между статусами. |
ExportJobType | Value Object / enum | Ограничивает допустимые типы экспорта и убирает "магические строки" из прикладного сценария. |
Описание:
- экспорт оформлен как отдельная доменная сущность, а не как разовая операция внутри контроллера;
- у export job есть явный lifecycle:
pending->processing->readyилиfailed; - статус, тип и payload вынесены в отдельные доменные объекты.
Как это выглядит на уровне application-слоя
Контроллеры не принимают прикладные решения напрямую, а собирают command/query и передают их в handler.
Пример связей между обработчиком в контроллере и командами домена:
AddRecipeToUserRationCommand->AddRecipeToUserRationHandler;AdjustMealPortionsCommand->AdjustMealPortionsHandler;GetUserDailyRationCommand->GetUserDailyRationHandler;ExportPdfDayRationJobCommand->ExportPdfDayRationJobHandler;DownloadPdfDayRationCommand->DownloadPdfDayRationHandler.
HTTP-слой остаётся точкой входа, а use case живёт отдельно и может переиспользоваться вне контроллера.
Ports / Adapters на практике
Прикладной слой работает через порты, а конкретная Laravel-инфраструктура подключается снизу.
Примеры портов:
ExportJobRepositoryPort;UserDayRationPdfJobQueuePort;- инфраструктурные реализации через
Eloquent, очередь, файловое хранилище иPDFbuilder.
Прикладной сценарий описывает, что нужно сделать, а не как именно Laravel это сохранит, поставит в очередь или отдаст файл.
Read / Write разделение
Делал разделение на read/write сценарии, например:
- чтение списков рецептов идёт через
ListRecipesQuery; - чтение сохранённых рецептов вынесено в отдельный query flow;
- изменение дневного рациона идёт через command/use-case handlers;
- экспорт и скачивание результата тоже оформлены как отдельные сценарии.
Отдельно использовал декораторы над query-handler-ами для кэширования read-моделей, например списков рецептов и сохранённых рецептов. Инфраструктурный кэш добавляется вокруг use case, не смешиваясь с доменной логикой.