Что такое богатая доменная модель (Rich Domain Model)?

Ответ

Богатая доменная модель — это архитектурный подход в 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 над каталогом товаров — может, и не надо городить. Но если у тебя там процессы, сложные проверки, агрегаты — без богатой модели потом будешь рыдать, пытаясь понять, в каком из двадцати сервисов спряталось правило, что нельзя добавлять товар из архивной категории в заказ со скидкой. Тут без вариантов.