Ваш 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, агре