Что такое Domain-Driven Design (DDD) и как его применять в Node.js?

Ответ

Domain-Driven Design (DDD) — это подход к разработке сложных бизнес-приложений, где основное внимание уделяется моделированию предметной области (бизнес-логики) в коде. В Node.js-проектах я применяю DDD для создания поддерживаемых и масштабируемых приложений.

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

  1. Сущность (Entity) — объект с уникальным идентификатором:

    class User {
    constructor(
    public readonly id: string,
    public email: string,
    private passwordHash: string
    ) {}
    
    changeEmail(newEmail: string) {
    // Бизнес-правило: валидация email
    if (!this.isValidEmail(newEmail)) {
      throw new Error('Invalid email');
    }
    this.email = newEmail;
    }
    }
  2. Объект-значение (Value Object) — неизменяемый объект без идентификатора:

    class Money {
    constructor(
    public readonly amount: number,
    public readonly currency: string
    ) {}
    
    add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error('Cannot add different currencies');
    }
    return new Money(this.amount + other.amount, this.currency);
    }
    }
  3. Агрегат (Aggregate) — группа связанных объектов с корневым Aggregate Root:

    class Order {
    private items: OrderItem[] = [];
    
    constructor(
    public readonly id: string,
    private customerId: string,
    private status: OrderStatus
    ) {}
    
    addItem(productId: string, quantity: number, price: Money) {
    // Инвариант: нельзя добавлять товары в завершенный заказ
    if (this.status === OrderStatus.COMPLETED) {
      throw new Error('Cannot modify completed order');
    }
    
    this.items.push(new OrderItem(productId, quantity, price));
    }
    
    calculateTotal(): Money {
    return this.items.reduce(
      (total, item) => total.add(item.calculateSubtotal()),
      new Money(0, 'USD')
    );
    }
    }

Практическое применение в Node.js:

  • Использую TypeScript для явного моделирования домена через интерфейсы и классы
  • Разделяю код на слои: Domain (чистая бизнес-логика), Application (координация), Infrastructure (база данных, внешние API)
  • Применяю паттерн Repository для доступа к данным, сохраняя доменную модель независимой от базы данных
  • Использую события домена (Domain Events) для связи между агрегатами

В последнем проекте электронной коммерции DDD помог нам четко отделить сложные бизнес-правила расчета скидок и налогов от инфраструктурного кода, что упростило тестирование и позволило быстрее реагировать на изменения требований бизнеса.

Ответ 18+ 🔞

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

Основные кирпичики этой вселенной в Node.js:

  1. Сущность (Entity) — это как паспорт, у него есть уникальный номер, и с ним можно что-то делать, но номер не меняется. Представь себе пользователя:

    class User {
    constructor(
    public readonly id: string, // Вот этот хуй с горы — его не перепишешь
    public email: string,
    private passwordHash: string
    ) {}
    
    changeEmail(newEmail: string) {
    // А тут бизнес-правило: если email — хуйня, то и не суйся
    if (!this.isValidEmail(newEmail)) {
      throw new Error('Invalid email');
    }
    this.email = newEmail;
    }
    }
  2. Объект-значение (Value Object) — это как наличные в кармане: нет идентификатора, просто сумма и валюта. Создал, использовал — выкинул, ёпта.

    class Money {
    constructor(
    public readonly amount: number,
    public readonly currency: string
    ) {}
    
    add(other: Money): Money {
    // Нельзя сложить доллары с рублями, это же бред, ядрёна вошь!
    if (this.currency !== other.currency) {
      throw new Error('Cannot add different currencies');
    }
    return new Money(this.amount + other.amount, this.currency);
    }
    }
  3. Агрегат (Aggregate) — это уже целая банда объектов, у которой есть главарь — Aggregate Root. Все ходят за ним и слушаются, а если кто-то левый суётся — получает в табло.

    class Order {
    private items: OrderItem[] = [];
    
    constructor(
    public readonly id: string,
    private customerId: string,
    private status: OrderStatus
    ) {}
    
    addItem(productId: string, quantity: number, price: Money) {
    // Инвариант, блядь: в завершённый заказ хуй что добавишь!
    if (this.status === OrderStatus.COMPLETED) {
      throw new Error('Cannot modify completed order');
    }
    
    this.items.push(new OrderItem(productId, quantity, price));
    }
    
    calculateTotal(): Money {
    return this.items.reduce(
      (total, item) => total.add(item.calculateSubtotal()),
      new Money(0, 'USD')
    );
    }
    }

Как я это в Node.js впариваю на практике:

  • Беру TypeScript и начинаю лепить домен из интерфейсов и классов, как из пластилина. Чтоб всё было строго и понятно, а не как у того полупидора, который всё в any оборачивает.
  • Дробил код на слои: Domain (там чистая бизнес-логика, святое место), Application (это как дирижёр, который всем рулит) и Infrastructure (тут уже базы данных, внешние API — всё, что пачкает руки).
  • Использую паттерн Repository, чтобы доменная модель жила в своём мире и не парилась, как там данные из базы выковыривают. Доверия ебать ноль к этим ORM, которые всё пытаются сами сделать.
  • Запускаю события домена (Domain Events), когда один агрегат другому должен сообщить новость. Типа «эй, заказ создан, можешь инвойс выписывать», а не чтобы они друг у друга из базы данные таскали, как последние мартышлюшки.

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