Domain-Driven Design в JavaScript: Cложный код? Наводим порядок как профи

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваш JavaScript-код превращается в спагетти? Пора внедрять DDD

Вы когда-нибудь ловили себя на мысли, что проект на JavaScript стал напоминать тарелку спагетти? Методы тянутся друг за другом, изменения в одном модуле ломают три других, а бизнес-логика размазана по контроллерам, сервисам и утилитам. Знакомо? Если ваш проект перерос стадию «просто прототип», самое время познакомиться с Domain-Driven Design (DDD) — предметно-ориентированным проектированием.

Многие думают, что DDD — это удел монстров на Java или C#. Но это миф. Domain-Driven Design в JavaScript проектах не только возможен, но и невероятно полезен. Он помогает разработчикам и бизнесу говорить на одном языке, а код становится предсказуемым и устойчивым к изменениям. В этой статье мы разберем DDD на практике, с примерами реального кода, чтобы вы могли применить это уже сегодня.

Что такое DDD и зачем он JavaScript-разработчику?

DDD — это не библиотека и не фреймворк. Это подход к проектированию программного обеспечения, который ставит в центр предметную область (домен) бизнеса. Вместо того чтобы думать о таблицах базы данных или REST API, вы думаете о сущностях бизнеса: «Заказ», «Клиент», «Счет», «Правило расчета скидки».

Главная боль, которую решает DDD — это сложность. Когда бизнес-логика живет в одном месте, а технические детали в другом, проект становится хрупким. DDD предлагает четкую архитектуру, которая отделяет зерна от плевел.

Для JavaScript, где мы часто используем динамическую типизацию и прототипы, DDD дает дисциплину. Он заставляет явно описывать типы данных и правила их изменения, что критически важно для масштабируемых приложений на Node.js или сложных клиентских приложений.

Основные строительные блоки DDD (с примерами на JS)

Давайте перейдем к самому интересному — коду. Мы напишем простую, но показательную систему управления заказами в интернет-магазине. Мы будем использовать современный JavaScript (ES6+).

1. Value Object (Объект-значение)

Это неизменяемый объект, который определяется своими атрибутами, а не идентичностью. Два Value Object равны, если у них одинаковые значения. Идеальный пример — деньги, адрес, email.

// value-objects/Money.js
class Money {
  constructor(amount, currency) {
    // Валидация — ключевая часть DDD
    if (typeof amount !== 'number' || amount < 0) {
      throw new Error('Сумма должна быть положительным числом');
    }
    if (!['USD', 'RUB', 'EUR'].includes(currency)) {
      throw new Error('Неподдерживаемая валюта');
    }

    this._amount = amount;
    this._currency = currency;
    // Замораживаем объект для иммутабельности
    Object.freeze(this);
  }

  get amount() { return this._amount; }
  get currency() { return this._currency; }

  // Бизнес-метод: сложение денег
  add(otherMoney) {
    if (this.currency !== otherMoney.currency) {
      throw new Error('Нельзя складывать разные валюты');
    }
    return new Money(this.amount + otherMoney.amount, this.currency);
  }

  equals(other) {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

// Использование
const price = new Money(100, 'USD');
const tax = new Money(20, 'USD');
const total = price.add(tax); // Money { _amount: 120, _currency: 'USD' }
console.log(total);

Почему это круто? Теперь сумма денег — не просто число, а полноценная сущность с правилами. Вы не сможете случайно сложить доллары с рублями или сделать сумму отрицательной.

2. Entity (Сущность)

В отличие от Value Object, у Сущности есть идентичность. Она может меняться со временем, но ее ID остается неизменным. Например, пользователь или заказ.

// entities/Order.js
const { v4: uuidv4 } = require('uuid'); // или crypto.randomUUID()

class Order {
  constructor(id, customerId) {
    this._id = id || uuidv4();
    this._customerId = customerId;
    this._items = [];
    this._status = 'pending'; // pending, confirmed, shipped
  }

  get id() { return this._id; }
  get status() { return this._status; }

  addItem(productId, quantity, price) {
    // Здесь мы используем наш Value Object
    this._items.push({
      productId,
      quantity,
      price: new Money(price, 'USD')
    });
  }

  // Бизнес-правило: заказ можно подтвердить только если он не пуст
  confirm() {
    if (this._items.length === 0) {
      throw new Error('Нельзя подтвердить пустой заказ');
    }
    this._status = 'confirmed';
  }
}

const order = new Order(null, 'user_123');
order.addItem('prod_1', 2, 50);
order.confirm();
console.log(order.status); // confirmed

3. Aggregate (Агрегат)

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

В нашем примере Order — это и есть корень агрегата. Вы не можете напрямую изменить _items извне. Все операции идут через методы addItem и confirm, которые гарантируют соблюдение бизнес-инвариантов.

// Пример: нельзя добавить товар в уже подтвержденный заказ
class Order {
  // ... предыдущий код
  addItem(productId, quantity, price) {
    if (this._status !== 'pending') {
      throw new Error('Нельзя изменить подтвержденный заказ');
    }
    // ... логика добавления
  }
}

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

Репозиторий — это посредник между доменом и хранилищем данных (база данных, API). Он предоставляет интерфейс для получения и сохранения агрегатов, скрывая детали реализации (SQL, MongoDB, файлы).

// repositories/OrderRepository.js
class OrderRepository {
  // Интерфейс: сохраняем весь агрегат целиком
  async save(order) {
    // Абстракция: мы не знаем, как именно сохраняется
    throw new Error('Метод save должен быть реализован');
  }

  async findById(id) {
    throw new Error('Метод findById должен быть реализован');
  }
}

// Реализация для MongoDB
class MongoOrderRepository extends OrderRepository {
  constructor(db) {
    super();
    this._collection = db.collection('orders');
  }

  async save(order) {
    await this._collection.replaceOne(
      { _id: order.id },
      { $set: this._toPersistence(order) },
      { upsert: true }
    );
  }

  _toPersistence(order) {
    return {
      id: order.id,
      customerId: order._customerId,
      status: order.status,
      items: order._items.map(item => ({
        productId: item.productId,
        quantity: item.quantity,
        price: item.price.amount, // Сохраняем только число
        currency: item.price.currency
      }))
    };
  }
}

Слой приложения и сервисы домена

В DDD есть четкое разделение на слои. Мы уже рассмотрели Доменный слой (сущности, value objects, агре

Блоги

Рекомендаций по книгам