Что такое DDD (Domain-Driven Design)?

Ответ

Domain-Driven Design (DDD) — это подход к разработке сложного программного обеспечения, который фокусируется на глубоком моделировании предметной области (бизнес-логики) и её отражении в коде. Основная цель — создание гибкой, понятной и легко изменяемой системы, которая эволюционирует вместе с бизнесом.

Ключевые строительные блоки DDD:

  1. Доменная модель (Domain Model): Сердце системы. Это не просто "база данных", а набор взаимосвязанных объектов (сущностей, value-объектов), которые инкапсулируют бизнес-правила и поведение.
  2. Универсальный язык (Ubiquitous Language): Общий язык для общения между разработчиками и экспертами предметной области. Термины из этого языка напрямую становятся названиями классов, методов и модулей в коде.
  3. Ограниченный контекст (Bounded Context): Чёткая граница, внутри которой определённая модель и универсальный язык являются согласованными. Большая система разбивается на такие контексты (например, "Заказы", "Доставка", "Каталог").
  4. Сущность (Entity): Объект, имеющий уникальный идентификатор, который остаётся неизменным на протяжении всего жизненного цикла (например, User с userId).
  5. Объект-значение (Value Object): Объект, не имеющий идентификатора и определяемый исключительно своими атрибутами (например, Money с amount и currency). Они неизменяемы (immutable).
  6. Агрегат (Aggregate): Кластер связанных сущностей и value-объектов, рассматриваемых как единое целое для операций изменения. У агрегата есть корень (Aggregate Root) — единственная сущность, через которую происходит всё внешнее взаимодействие.
  7. Репозиторий (Repository): Абстракция, предоставляющая доступ к агрегатам, скрывая детали хранения данных (например, IOrderRepository).
  8. Доменный сервис (Domain Service): Операция или логика, которая по своей природе не принадлежит ни одной сущности или value-объекту (например, сложный расчёт стоимости доставки, затрагивающий несколько агрегатов).

Практический пример на C# (контекст "Заказы"):

// Value Object
public record Address(string Street, string City, string PostalCode);

// Entity
public class OrderItem
{
    public ProductId ProductId { get; private set; }
    public Money Price { get; private set; }
    public int Quantity { get; private set; }
    // ... логика создания и изменения
}

// Aggregate Root
public class Order : AggregateRoot<OrderId>
{
    private readonly List<OrderItem> _items = new();
    public CustomerId CustomerId { get; private set; }
    public Address ShippingAddress { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

    public void AddItem(Product product, int quantity)
    {
        // Бизнес-правило: нельзя добавить товар с нулевым количеством
        if (quantity <= 0) throw new DomainException("Quantity must be positive.");

        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existingItem != null)
        {
            // Если товар уже есть, увеличиваем количество
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            // Иначе создаём новый элемент заказа
            _items.Add(new OrderItem(product.Id, product.Price, quantity));
        }
        // Генерируем доменное событие OrderItemAdded
        AddDomainEvent(new OrderItemAdded(this.Id, product.Id, quantity));
    }
}

// Repository Interface (определяется в доменном слое)
public interface IOrderRepository : IRepository<Order>
{
    Task<Order?> GetByIdAsync(OrderId id, CancellationToken cancellationToken = default);
    Task AddAsync(Order order, CancellationToken cancellationToken = default);
}

DDD не является серебряной пулей и требует значительных усилий. Он наиболее оправдан в проектах со сложной, постоянно меняющейся бизнес-логикой, где правильность и ясность кода напрямую влияют на успех бизнеса.

Ответ 18+ 🔞

Слушай, а вот есть такая штука — Domain-Driven Design, или DDD, если по-пацански. Это, блядь, не просто очередной модный фреймворк, который за неделю выучил и щёлкаешь как орехи. Это, ёпта, целая философия, как не сойти с ума, когда пишешь какую-нибудь пиздопроебибну систему для банка или логистики, где правил больше, чем у деда в гараже.

Основная идея — перестать, наконец, переводить требования бизнеса в какие-то свои кривые таблички в базе, а начать говорить на одном языке с этими самыми бизнес-экспертами. Представляешь? Ты приходишь к ним, а они тебе: «Нам нужно, чтобы заказ резервировал инвентарь, но только после валидации кредитного лимита, и если лимит превышен, то запускается процесс ручного одобрения». И ты вместо того, чтобы кивать и потом неделю ебаться с непонятными полями в БД, говоришь: «Окей, значит у нас есть Агрегат Заказ, который вызывает Доменный Сервис ПроверкаКредита, и если всё плохо — кидает Событие ЗаказТребуетОдобрения». И они такие: «Да, бля, именно так!». Вот это и есть Универсальный язык. И эти же термины потом и в коде будут. Красота же!

А теперь по косточкам, что там за кирпичики в этой стройке:

  1. Доменная модель — это, сука, святое. Это не ваша база данных, нет. Это живое воплощение всех этих бизнес-правил, страхов и хотелок в виде кода. Если в бизнесе говорят «деньги нельзя просто так взять и поделить, там курс валюты», то в коде у тебя будет объект Money с полями Amount и Currency, а не просто decimal totalPrice. Понимаешь разницу? Доверия ебать ноль к голым числам.

  2. Ограниченный контекст — вот это вообще гениальная мысль. Это признание того, что «пользователь» в контексте «Доставки» и «пользователь» в контексте «Оплаты» — это две разные, блядь, сущности. В одном от него нужен только адрес, а в другом — номер карты. И не надо лепить из этого одного монстра на все случаи жизни, который потом хуй с горы не сдвинешь. Разбиваешь систему на такие изолированные модули-контексты, и в каждом живёт своя модель со своим языком.

  3. Сущность и Объект-значение — основа основ. Сущность — это когда важна идентичность. User с ID — он один, уникальный, его можно найти и отследить. А Address (улица, город, индекс) — это объект-значение. Два адреса с одинаковыми полями — это один и тот же адрес, да похуй на их ID. Value Object'ы должны быть неизменяемыми, как скала. Создал — и забыл. Это чтобы потом в 3 часа ночи не искать, кто и где перезаписал тебе город в объекте.

  4. Агрегат — это, можно сказать, банда. Группа связанных сущностей, которые всегда действуют заодно. Например, Заказ (корень агрегата) и все его ПозицииЗаказа. Нельзя просто так взять и изменить позицию в обход заказа. Все изменения — только через главного, через корень (Order). Это чтобы сохранять целостность, а не получить потом хитрую жопу, где данные друг другу противоречат.

  5. Репозиторий — это такая умная обёртка над твоим способом хранения (база, файл, облако). Ты в доменной логике просто говоришь: orderRepository.AddAsync(myOrder). А уж это сохранится в SQL, в MongoDB или в оперативку — твоему домену абсолютно похуй. Главное — контракт.

  6. Доменный сервис — это для операций, которые как бы всем мешают. Ну вот, например, логика «рассчитать стоимость доставки». Она нужна и заказу, и корзине, и окошку с предпросчётом. Выносишь её в отдельный сервис DeliveryCostCalculator и пользуешься, где надо. Чисто, без привязки к какой-то одной сущности.

Ну и смотри, как это может выглядеть в коде, на примере того же заказа:

// Value Object — просто набор данных, неизменяемый. Сравниваются по полям.
public record Address(string Street, string City, string PostalCode);

// Aggregate Root — главный в агрегате. Все изменения через него.
public class Order : AggregateRoot<OrderId>
{
    private readonly List<OrderItem> _items = new();
    public Address ShippingAddress { get; private set; }

    // Основная бизнес-логика тут
    public void AddItem(Product product, int quantity)
    {
        // Правило: ноль или отрицательное количество — никуда не годится.
        if (quantity <= 0) throw new DomainException("Ты чё, мудила? Количество должно быть больше нуля!");

        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existingItem != null)
        {
            // Если уже есть в заказе — увеличиваем количество.
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            // Иначе — создаём новую позицию.
            _items.Add(new OrderItem(product.Id, product.Price, quantity));
        }
        // Генерируем событие, что что-то поменялось. Это для других частей системы.
        AddDomainEvent(new OrderItemAdded(this.Id, product.Id, quantity));
    }
}

Короче, DDD — это мощно, но это овердохуища работы. Это не для стартапа из трёх человек, где надо за неделю наколбасить прототип. Это оружие для сложных, долгоживущих проектов, где бизнес-логика — это твой главный враг и лучший друг одновременно. Если применять с умом — система получается гибкой, как гимнастка, и понятной, как два пальца. Если халтурить — получится просто манда с ушами, перегруженная абстракциями. Выбор за тобой.