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

Ответ

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

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

  1. Ubiquitous Language (Единый язык):

    • Создание общего словаря терминов между разработчиками, экспертами предметной области (domain experts) и всеми участниками проекта.
    • Этот язык используется в коде, документации и обсуждениях, устраняя недопонимание.
    • Пример: В банковской сфере термины "Счет", "Клиринг", "Овердрафт" имеют четкие, согласованные определения.
  2. Bounded Context (Ограниченный контекст):

    • Явное определение границ, внутри которых конкретная модель и единый язык являются согласованными и применимыми.
    • Один и тот же термин в разных контекстах может иметь разное значение.
    • Пример: Сущность Product в контексте "Каталог" имеет атрибуты Name, Description, Price. В контексте "Логистика" та же сущность Product имеет атрибуты Weight, Dimensions, StorageRequirements.
  3. Aggregate (Агрегат) и Aggregate Root (Корень агрегата):

    • Агрегат — это кластер из связанных объектов (сущностей и value objects), который трактуется как единое целое для операций изменения данных.
    • Aggregate Root (AR) — единственная точка входа в агрегат. Внешние объекты могут ссылаться только на AR, а не на внутренние сущности. AR отвечает за инварианты (бизнес-правила) всего агрегата.

      
      // Aggregate Root
      public class Order : AggregateRoot
      {
      public Guid Id { get; private set; }
      public OrderStatus Status { get; private set; }
      private readonly List<OrderLine> _lines = new(); // Внутренняя сущность
      public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
      
      // Все изменения идут через методы AR
      public void AddLine(Product product, int quantity)
      {
          // Проверка инварианта: нельзя добавлять товары в завершенный заказ
          if (Status == OrderStatus.Completed)
              throw new DomainException("Cannot modify a completed order.");
      
          var line = new OrderLine(product.Id, product.Price, quantity);
          _lines.Add(line);
      
          // Генерация Domain Event
          AddDomainEvent(new OrderLineAddedEvent(Id, product.Id, quantity));
      }
      
      public void MarkAsCompleted() { ... }
      }

    // Value Object (неизменяемый) public record OrderLine : ValueObject { public Guid ProductId { get; } public Money Price { get; } // Другой Value Object public int Quantity { get; } public Money Total => Price * Quantity; // Конструктор и equality на основе всех полей }

  4. Repository (Репозиторий):

    • Абстракция, которая инкапсулирует логику доступа к агрегатам (обычно через персистентность).
    • Работает только с Aggregate Roots.
    • IOrderRepository.GetByIdAsync(id) возвращает Order (AR) со всеми его OrderLines.
  5. Domain Event (Доменное событие):

    • Факт, что в домене что-то значимое произошло (например, OrderPlacedEvent, PaymentReceivedEvent).
    • Позволяет разным частям системы (внутри или даже вне bounded context) реагировать на изменения, не создавая жестких зависимостей.
  6. Слоистая/Гексагональная архитектура:

    • DDD часто реализуется с четким разделением на Domain Layer (чистая бизнес-логика, без зависимостей от инфраструктуры), Application Layer (координация задач, транзакций), Infrastructure Layer (реализация репозиториев, вызов внешних сервисов).

Когда использовать DDD?

  • Проекты со сложной и нетривиальной бизнес-логикой.
  • Когда требуется долгосрочная поддержка и активная эволюция продукта.
  • Когда критично точное соответствие программной модели реальным бизнес-процессам.

Когда НЕ использовать DDD?

  • Простые CRUD-приложения без сложных бизнес-правил.
  • Прототипы или проекты с очень коротким сроком жизни.
  • Ситуации, где накладные расходы на сложность архитектуры не окупаются.

Ответ 18+ 🔞

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

Основные фишки, без которых нихуя не понятно:

  1. Единый язык (Ubiquitous Language):

    • Это когда вы с экспертами и между собой договариваетесь, что «клиент» — это вот это, «заявка» — это вот так, а «просрочка» — это вот эта хуйня. И этот же самый язык ты потом пишешь в коде. Чтобы не было такого, что ты говоришь «юзер», а он имеет в виду «аккаунт», и вы оба думаете, что поняли друг друга, а на деле — пиздец и два дня дебага.
  2. Ограниченный контекст (Bounded Context):

    • Это, блядь, самое важное. Один и тот же термин в разных отделах — это разные сущности. Вот смотри: «Товар» для отдела продаж — это картинка, название и цена, чтобы быстрее впарить. А «Товар» для склада — это габариты, вес и температура хранения, чтобы он не сгнил. Это два РАЗНЫХ контекста! И пытаться слепить из них одну универсальную хуйню на все случаи жизни — это прямой путь в ад. Делай две разные модели и не мучайся.
  3. Агрегат и Корень агрегата (Aggregate & Aggregate Root):

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

      // Вот он, Корень агрегата. Царь и бог.
      public class Order : AggregateRoot
      {
      public Guid Id { get; private set; }
      private List<OrderLine> _lines = new(); // Список позиций — внутри, приватный.
      
      // Хочешь добавить позицию? Делай только так!
      public void AddItem(Product product, int quantity)
      {
          // А вот и бизнес-правило: в отменённый заказ хуй что добавишь.
          if (Status == OrderStatus.Cancelled)
              throw new DomainException("Иди нахуй, заказ отменён!");
      
          _lines.Add(new OrderLine(product.Id, quantity));
      }
      // Больше никто не может _lines тронуть. Красота.
      }
  4. Репозиторий (Repository):

    • Это такая прослойка, которая делает вид, что твои агрегаты живут в какой-то волшебной коллекции в памяти. Хочешь заказ? orderRepository.Get(id). Сохранить? orderRepository.Save(order). А то, что там внутри база данных, кэш или файл на диске — это твои личные проблемы. Репозиторий работает только с Корнями агрегатов, запомни это.
  5. Доменное событие (Domain Event):

    • Это когда в домене происходит что-то важное, о чём должны узнать другие. Заказ создан? Вызывай событие OrderCreatedEvent. И пусть другие модули (например, служба нотификаций или склад) на него подписываются и делают что надо. Главное — не превращать это в спагетти-код, где всё зависит от всего.
  6. Архитектура:

    • Всё это безобразие обычно раскладывают по слоям. Доменный слой — это святое. Только бизнес-логика, ни одной ссылки на базу данных, HTTP или прочую инфраструктуру. Слой приложения — тут уже координация, вызов репозиториев. Инфраструктурный слой — тут уже все эти грязные подробности работы с внешним миром.

Когда это всё городить? Да когда проект реально сложный, с кучей правил, которые ещё и меняются каждые полгода. Когда от твоего кода зависит, будет компания в плюсе или в ауте.

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