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

«Что такое DDD (Domain-Driven Design)?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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 не является серебряной пулей и требует значительных усилий. Он наиболее оправдан в проектах со сложной, постоянно меняющейся бизнес-логикой, где правильность и ясность кода напрямую влияют на успех бизнеса.