Ответ
Богатая доменная модель — это архитектурный подход в Domain-Driven Design (DDD), при котором бизнес-логика (поведение) инкапсулирована внутри самих сущностей и объектов-значений предметной области, а не размещается в отдельном сервисном слое (так называемая "анемичная модель").
Суть подхода: Доменные объекты — это не просто "контейнеры для данных" (DTO), а полноценные объекты с методами, которые реализуют правила и инварианты предметной области.
Сравнение с анемичной моделью:
// АНЕМИЧНАЯ МОДЕЛЬ (Anti-pattern): Данные и логика разделены.
public class Order // Только данные, геттеры/сеттеры
{
public int Id { get; set; }
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
public class OrderService // Вся логика вынесена в сервис
{
public void AddItem(Order order, Product product, int qty)
{
// Проверки и расчеты здесь
if (qty <= 0) throw new Exception();
order.Items.Add(new OrderItem(product, qty));
order.Total += product.Price * qty;
}
}
// БОГАТАЯ ДОМЕННАЯ МОДЕЛЬ: Логика внутри сущности.
public class Order
{
public int Id { get; private set; } // Сеттер приватный
public decimal Total { get; private set; }
private readonly List<OrderItem> _items = new(); // Инкапсуляция коллекции
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
// Бизнес-метод, отвечающий за добавление товара. Инкапсулирует правила.
public void AddItem(Product product, int quantity)
{
// 1. Проверка инвариантов (валидация внутри домена)
if (product == null) throw new ArgumentNullException(nameof(product));
if (quantity <= 0) throw new ArgumentException("Количество должно быть положительным", nameof(quantity));
if (!product.IsInStock) throw new InvalidOperationException("Товара нет в наличии");
// 2. Создание объекта-значения OrderItem
var item = new OrderItem(product.Id, product.Price, quantity);
// 3. Изменение состояния сущности
_items.Add(item);
Total += item.Subtotal;
// 4. Возможность генерации доменного события
// DomainEvents.Raise(new OrderItemAddedEvent(this, item));
}
// Другой бизнес-метод
public void ApplyDiscount(decimal discountPercent)
{
if (discountPercent < 0 || discountPercent > 100)
throw new ArgumentException("Неверный процент скидки");
Total *= (1 - discountPercent / 100);
}
}
Преимущества богатой модели:
- Высокая связность: Данные и операции над ними находятся в одном месте.
- Инкапсуляция: Состояние объекта защищено от некорректного изменения извне.
- Выразительность: Код моделирует реальные бизнес-процессы, его легче понимать.
- Тестируемость: Легко тестировать единицы бизнес-логики в изоляции.
Когда использовать? В сложных проектах с насыщенной и часто меняющейся бизнес-логикой, где важна чистота архитектуры и долгосрочная поддерживаемость.
Ответ 18+ 🔞
Давай разберем эту вашу "богатую доменную модель", а то как посмотрю на код некоторых проектов — волосы дыбом, ебать мои старые костыли.
Смотри, есть два пути. Первый — это когда твои сущности выглядят как беспомощные мешки с данными, тупо набор полей с геттерами-сеттерами. Вся логика размазана по сервисам, которые знают про твою предметную область вообще всё. Это называется анемичная модель, и это, прости меня господи, антипаттерн. Выглядит это пиздец как просто, но потом поддерживать этот цирк — терпения ноль ебать.
А есть второй путь — богатая модель. Суть в чём? Ты берёшь свою бизнес-сущность, например, Заказ, и делаешь из неё не тупой контейнер, а полноценного умного мужика, который сам знает, что с ним можно делать, а что нет. Вся логика живёт внутри него, а не снаружи.
Вот смотри, как это бывает:
// АНЕМИЧНЫЙ УРОДЕЦ (так делать не надо, я серьёзно)
public class Order
{
public int Id { get; set; } // Любой может поменять ID, а зачем?
public decimal Total { get; set; } // И итог может кто угодно поправить
public List<OrderItem> Items { get; set; } = new(); // Публичная коллекция? Ну-ну.
}
// А логика где? А логика в каком-то OrderService на 5000 строк, который знает ВСЁ.
А теперь богатый вариант:
public class Order
{
public int Id { get; private set; } // ID задаётся один раз при создании, и хватит
public decimal Total { get; private set; } // Сумма меняется только внутренними методами
private readonly List<OrderItem> _items = new(); // Коллекция спрятана, чтоб не лапали
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly(); // Наружу даём только для чтения
// Вот она, соль! Метод предметной области. Заказ САМ умеет добавлять товар.
public void AddItem(Product product, int quantity)
{
// Проверяем инварианты прямо здесь. Не в сервисе, не в контроллере — ЗДЕСЬ.
if (product == null) throw new ArgumentNullException(nameof(product));
if (quantity <= 0) throw new ArgumentException("Количество должно быть положительным, дурик", nameof(quantity));
if (!product.IsInStock) throw new InvalidOperationException("Товара нет в наличии, иди отсюда");
// Создаём позицию заказа (это скорее всего объект-значение)
var item = new OrderItem(product.Id, product.Price, quantity);
// Меняем своё внутреннее состояние
_items.Add(item);
Total += item.Subtotal; // И автоматически пересчитываем сумму
// Если надо, можем даже событие доменное поднять, что позиция добавилась
// DomainEvents.Raise(new OrderItemAddedEvent(this, item));
}
// И другой метод, например, применить скидку
public void ApplyDiscount(decimal discountPercent)
{
if (discountPercent < 0 || discountPercent > 100)
throw new ArgumentException("Да ты что, скидка не может быть такой, охуел?");
Total *= (1 - discountPercent / 100);
}
}
Чувствуешь разницу? В первом случае Order — это просто глупая структура данных. Чтобы добавить товар, тебе нужен OrderService, который знает про Product, про валидацию, про пересчёт итога. Получается спагетти-код, где логика заказа размазана по десяти сервисам.
Во втором случае Order — это как бы самостоятельная единица. Он сам отвечает за свою целостность. Хочешь добавить товар? Вызови у заказа метод AddItem и передай товар с количеством. Он сам всё проверит, сам создаст позицию, сам пересчитает сумму. Никакой посторонний сервис не может взять и тупо залезть в его коллекцию _items или поменять Total как попало. Инкапсуляция, детка!
Зачем это всё?
- Связность: Всё, что связано с жизнью заказа, лежит в одном классе, а не раскидано по проекту.
- Защита от дурака: Состояние объекта нельзя сломать извне, вызывая сеттеры подряд. Менять его можно только через понятные бизнес-методы.
- Ясность: Читая код домена, ты сразу понимаешь правила бизнеса. "О, смотри, чтобы добавить товар, он должен быть в наличии. Логично!"
- Тесты писать — одно удовольствие: Тестируешь не связку из сервиса, репозитория и ещё хз чего, а просто один объект
Orderи его методы.
Когда это нужно? Да когда бизнес-логика сложная, запутанная и часто меняется. Если у тебя простой CRUD над каталогом товаров — может, и не надо городить. Но если у тебя там процессы, сложные проверки, агрегаты — без богатой модели потом будешь рыдать, пытаясь понять, в каком из двадцати сервисов спряталось правило, что нельзя добавлять товар из архивной категории в заказ со скидкой. Тут без вариантов.