Опубликовано
- 4 мин чтения
Разбираемся в DDD: от анемичной к богатой доменной модели

Недавно я прочитал книгу Learning Domain-Driven Design по совету товарища, который более опытен в разработке, чем я. Я уже написал небольшой обзор об этой книге в своем Telegram-канале. Теперь я собираюсь написать серию статей о том, как Domain-Driven Design (DDD) может улучшить ваш код и процесс разработки. Это первая статья в этой серии.
Анемичная доменная модель (ADM)
Не уверен, что я смогу описать ADM лучше, чем Мартин Фаулер. В первую очередь, очень рекомендую прочитать его статью об этом антипаттерне.
По сути, ADM — это доменная модель, в которой отсутствует поведение: она состоит только из геттеров и сеттеров. Я создал простое CLI-приложение, использующее ADM. Код основан на том, что я видел за свою карьеру разработчика, и мы попробуем его улучшить.
Приложение имитирует систему учёта тикетов. Оно содержит следующие модели:
Worker
— человек, ответственный за решение тикетов.Ticket
— сущность, описывающая проблему, которая может быть назначена любому сотруднику.
Функциональность приложения позволяет:
- Добавлять сотрудников.
- Обновлять данные сотрудников.
- Увольнять сотрудников.
- Получать информацию о сотрудниках.
- Открывать тикеты для сотрудников.
- Закрывать тикеты для сотрудников.
Представьте, что у нас есть тикет с Id = 1
, и пользователь хочет обновить его Description
. Он выполняет:
dotnet run -- ticket update -id 1 -c "Some new content"
Тикет будет обновлён, но что происходит внутри приложения? Ниже диаграмма последовательности для этого процесса:

Рисунок 1 — Диаграмма последовательности для обновления тикета
Выглядит довольно сложно, не так ли? В чём недостатки такой архитектуры:
- Доменные модели и объекты базы данных практически идентичны. Нарушается принцип DRY.
- Для реализации новой функциональности придётся писать минимум в два раза больше кода.
- Поддержка существующего кода будет занимать в два раза больше времени.
- Для каждой пары “доменная модель — объект БД” придётся писать мапперы. Больше кода — выше шанс ошибок.
- Лишние обращения к БД.
- При обработке сотен или тысяч сущностей нужно загружать все объекты БД в память и маппить их в доменные модели, что ухудшает производительность.
Как можно улучшить этот код? Посмотрим на диаграмму последовательности для открытия нового тикета:

Рисунок 2 — Диаграмма последовательности для открытия тикета
В этом примере нет лишних запросов к БД, но остальные проблемы остались. Где здесь бизнес-логика? Правильно — она размазана по разным обработчикам и просочилась в DAL.
Можно ли сделать ещё лучше? Да — с помощью богатой доменной модели (RDM).
Богатая доменная модель (RDM)
Как уже говорилось, ADM — это просто класс с набором геттеров и сеттеров. В отличие от неё, RDM содержит функциональность, связанную с бизнес-логикой.
Чтобы преобразовать ADM в RDM, нужно:
- Избавиться от дублирующихся моделей и мапперов, объединив доменные модели и объекты БД.
- Заменить
Requests
иQueries
наCommands
иEvents
. - Собрать все действия с БД в одном командном обработчике.
- Перенести бизнес-логику в доменные модели.
Избавляемся от дублирования
Первым делом объединим дублирующиеся модели. Например, Worker
:
public record Worker
{
public int Id { get; protected set; }
public string Name { get; protected set; } = string.Empty;
public string Email { get; protected set; } = string.Empty;
public string Position { get; protected set; } = string.Empty;
public bool Fired { get; protected set; }
public DateTimeOffset Created { get; protected set; }
public DateTimeOffset Updated { get; protected set; }
public ICollection<Ticket> AssignedTickets { get; protected set; } = new List<Ticket>();
}
Теперь модель одна, свойства защищены (protected set
), что предотвращает неконтролируемые изменения снаружи.
Команды и события
Команды (Commands
) описывают действие, которое изменяет доменную модель. События (Events
) описывают результат изменения.
Именование:
- Команды — глагол в повелительном наклонении + сущность (
CreateWorker
,UpdateWorker
). - События — сущность + глагол в прошедшем времени (
WorkerCreated
,WorkerUpdated
).
Единый обработчик команд
Теперь создадим единый CommandHandler:
public class CommandHandler
{
public EventBase Handle(CommandBase command)
{
var @event = command switch
{
TicketCommand cmd => Handle(cmd),
WorkerCommand cmd => Handle(cmd),
_ => throw new NotSupportedException(),
};
return @event;
}
}
Метод Handle
обрабатывает команды по общему шаблону: извлечение сущности из БД → вызов бизнес-метода → сохранение изменений → возврат события.
Обработчики в доменной модели
Теперь обработчики логики живут внутри самих моделей. Например:
public EventBase Handle(UpdateWorker request)
{
if (request.Name != null) Name = request.Name;
if (request.Email != null) Email = request.Email;
if (request.Position != null) Position = request.Position;
if (request.Name != null || request.Email != null || request.Position != null)
Updated = DateTimeOffset.Now;
return new WorkerUpdated(this);
}
После рефакторинга
Теперь посмотрим на диаграмму обновления тикета:

Рисунок 3 — Диаграмма обновления тикета через RDM
Стало значительно проще! И такая схема работает для всех команд.
Вывод
DDD и богатая доменная модель (RDM) помогают:
- Инкапсулировать данные и поведение.
- Использовать понятные соглашения об именовании.
- Избавиться от дублирования кода и мапперов.
- Минимизировать обращения к БД.
Это делает код проще, понятнее и устойчивее к изменениям.