Почему DDD — это не модный термин, а инструмент спасения проекта
Каждый Java-разработчик хоть раз сталкивался с ситуацией: код работает, бизнес-логика размазана по сервисам, а через полгода разобраться в проекте может только его автор (и то с трудом). Знакомо? Это классический anemic domain model — когда сущности — это просто POJO с геттерами и сеттерами, а вся логика живёт в «толстых» сервисах.
Здесь на сцену выходит Domain-Driven Design (DDD) — подход, который ставит в центр разработки бизнес-логику, а не технические детали. И нет, это не очередная «теоретическая» методология. DDD в Java проектах даёт конкретные паттерны, которые превращают хаос в чистую, поддерживаемую архитектуру. В этой статье мы разберём DDD на практике: с кодом, примерами и без воды.
Стратегический vs Тактический DDD: что выбрать для Java-проекта?
DDD делится на два уровня: стратегический и тактический. Первый помогает разбить большую систему на Bounded Contexts (контексты) и выстроить коммуникацию между ними. Второй — даёт конкретные строительные блоки для кода: Entity, Value Object, Aggregate, Repository, Domain Event и т.д.
Для Java-разработчика тактический DDD — это то, с чего стоит начать. Он даёт инструменты, которые можно применить уже сегодня. Но без стратегического понимания контекстов вы рискуете создать «универсальную» модель, которая не подходит никому. Поэтому давайте начнём с малого: разберём ключевые паттерны на примере простого, но реального домена — системы управления заказами.
Строительные блоки DDD на Java: от Entity до Aggregate
1. Entity (Сущность) — у каждой есть своя идентичность
В DDD Entity — это объект, который имеет уникальный идентификатор и непрерывную жизненную линию. Вы можете изменить все его поля, но это будет та же самая сущность. В Java это не просто POJO — это объект с поведением.
Пример: заказ в интернет-магазине. Даже если статус заказа меняется, это всё тот же заказ с тем же ID.
public class Order {
private final OrderId id; // Value Object, не примитив
private OrderStatus status;
private List<OrderLineItem> items;
private Money totalAmount;
public Order(OrderId id) {
this.id = id;
this.status = OrderStatus.PENDING;
this.items = new ArrayList<>();
this.totalAmount = Money.ZERO;
}
public void addItem(Product product, int quantity) {
// Бизнес-правило: нельзя добавить товар в подтверждённый заказ
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot add items to a non-pending order");
}
OrderLineItem item = new OrderLineItem(product, quantity);
items.add(item);
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm an empty order");
}
this.status = OrderStatus.CONFIRMED;
// Здесь можно запустить доменное событие
// registerEvent(new OrderConfirmedEvent(this.id));
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderLineItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// Геттеры — только для чтения, без сеттеров!
public OrderId getId() { return id; }
public OrderStatus getStatus() { return status; }
public List<OrderLineItem> getItems() { return Collections.unmodifiableList(items); }
public Money getTotalAmount() { return totalAmount; }
}
Обратите внимание: OrderId — это не Long, а Value Object. Почему? Потому что примитивный тип не несёт семантики. OrderId может содержать префикс, дату создания или просто быть UUID — детали реализации скрыты.
2. Value Object (Объект-значение) — неизменяемость и равенство по значению
Value Object (VO) — это объект, который определяется своими атрибутами, а не идентификатором. Два VO равны, если все их поля совпадают. Они должны быть иммутабельными (неизменяемыми).
Классический пример: Money и Address.
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
// Бизнес-правило: сумма не может быть отрицательной
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount;
this.currency = Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// equals и hashCode — обязательны, основаны на всех полях
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
// Геттеры — только для чтения
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
}
Использование Money вместо BigDecimal + String делает код самодокументируемым и защищает от ошибок валют.
3. Aggregate (Агрегат) — граница транзакционной согласованности
Aggregate — это кластер объектов (Entity и Value Object), которые рассматриваются как единое целое. У агрегата есть корневая сущность (Aggregate Root), через которую происходят все изменения. Внешние объекты могут ссылаться только на корень, но не на его внутренности.
В нашем примере Order — это корень агрегата. OrderLineItem — внутренняя сущность (или VO), которая не имеет смысла вне заказа. Никто не должен напрямую изменять items — только через методы Order.
public class OrderLineItem {
private final Product product; // ссылка на другой агрегат по ID
private final int quantity;
private final Money price; // цена на момент добавления
public OrderLineItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
this.price = product.getPrice();
}
public Money getSubtotal() {
return price.multiply(quantity);
}
// equals/hashCode — по product и quantity
}
Правило: внутри одного агрегата — одна транзакция. Если вы сохраняете заказ, сохраняется всё: и сам заказ, и его позиции. Это гарантирует согласованность данных.
Repository: как сохранять агрегаты в Java
Repository — это коллекция агрегатов, которая обеспечивает доступ к ним и их сохранение. Важно: репозиторий работает только с корнями агрегатов. Вы не сохраняете OrderLineItem отдельно — только через
Блоги
Рекомендаций по книгам
Читайте также