Принципы проектирования в DDD
Принципы проектирования в DDD: инварианты, границы транзакций, eventual consistency.
принципы проектирования DDD — паттерны DDD (Entity, Aggregate, Bounded Context) отвечают на вопрос «что строить». Эта статья — о другом: как думать при проектировании доменной модели. Здесь собраны принципы из книги Эрика Эванса, которые не укладываются в отдельные паттерны, но определяют качество архитектуры.
Если вы ещё не знакомы с паттернами DDD, начните с вводной статьи. Стратегические, тактические и интеграционные паттерны разобраны в отдельных статьях.
принципы проектирования DDD: 1. Перемалывание знаний (Knowledge Crunching)
Модель — это совместный артефакт разработчиков и доменных экспертов. Она растёт из диалога и постоянно уточняется. Важнее понять причинно-следственные связи в домене, чем быстро построить «правильные» классы.
Практики
- Совместные модел-сессии (whiteboard, Event Storming)
- Быстрый прототип → обратная связь → изменение модели
- Явное именование правил и политик
// ✅ Явная доменная категория — знание в коде
class CustomerTierResolver {
CustomerTier resolve(Customer customer) {
if (customer.ordersInLastYear() >= 20) return CustomerTier.GOLD;
if (customer.ordersInLastYear() >= 5) return CustomerTier.SILVER;
return CustomerTier.BRONZE;
}
}
// ❌ Знание спрятано в SQL — домен «протекает» в DAO
class OrderDao {
RiskRating riskFor(OrderId id) {
// SELECT ... CASE WHEN ...
return RiskRating.LOW;
}
}
// ❌ Копирование структуры БД вместо модели
class OrderRecord {
Long id;
BigDecimal total_amount;
String status_code;
// никаких правил и поведения
}
Главный риск: зафиксировать модель слишком рано или скопировать структуру БД как доменную модель. Ранние модели наивны — и это нормально, их нужно активно пересматривать.
2. Связь модели и реализации (Model-Driven Design)
Модель должна быть выражена в коде, иначе язык и дизайн расходятся. Если модель существует только на вики или в голове аналитика — это не Model-Driven Design.
// ✅ Model-Driven Design: код = модель
class Order {
private OrderStatus status;
void confirm() {
if (!canBeConfirmed())
throw new IllegalStateException("Order cannot be confirmed");
status = OrderStatus.CONFIRMED;
}
void markAsPaid() {
status = OrderStatus.PAID;
events.add(new OrderPaidEvent(id));
}
}
// ❌ Анемичная модель: сущность без поведения + сервис-контейнер логики
class Order {
Long id;
OrderStatus status;
/* только get/set */
}
class OrderService {
void confirm(Order order) { /* вся логика здесь */ }
}
Анемичная модель — самая частая причина деградации доменной логики. Бизнес-правила утекают в сервисы, контроллеры, ORM-маппинги, и со временем непонятно, где искать правду.
3. Изоляция домена (слоистая архитектура)
Слои защищают домен от инфраструктуры и UI. Направление зависимостей: UI → Application → Domain → Infrastructure. Доменные правила не должны зависеть от фреймворков.
// ✅ Application Service координирует, домен решает
class OrderAppService {
private final OrderRepository repo;
void confirmOrder(OrderId id) {
Order order = repo.byId(id);
order.confirm(); // решение — в домене
repo.save(order);
}
}
// ✅ Репозиторий: интерфейс в домене, реализация в инфраструктуре
class JpaOrderRepository implements OrderRepository {
public Order byId(OrderId id) { /* JPA */ }
public void save(Order order) { /* JPA */ }
}
// ❌ Домен зависит от инфраструктуры
@Entity
class Order { /* JPA-аннотации внутри домена */ }
// ❌ Application Service работает напрямую с EntityManager
class OrderAppService {
@PersistenceContext EntityManager em;
}
4. Прорывы (Breakthroughs)
Прорыв — это изменение модели, которое резко упрощает дизайн. Обычно это происходит, когда вы замечаете скрытую концепцию и делаете её явной. До прорыва код сложный и хрупкий. После — проще и выразительнее.
// ❌ До прорыва: скидка — просто флаг
class Order {
boolean hasDiscount;
boolean canCancel;
}
// ✅ После прорыва: скидка и политика отмены — доменные концепции
class Discount {
private final Money amount;
boolean appliesTo(Order order) { /* ... */ }
}
class CancellationPolicy {
boolean canCancel(Order order) { /* ... */ }
}
class Order {
void apply(Discount discount) { /* ... */ }
}
Как искать прорывы: ищите скрытые концепции — роли, политики, периоды, статусы. Если участок модели «слишком сложный» — это кандидат на переосмысление. Прорыв требует рефакторинга, даже если больно. Главный риск — держаться за старую модель из-за стоимости изменений.
5. Делать скрытое явным
Важные правила и концепции домена должны стать объектами модели, а не прятаться в if/else. Кандидаты на выделение: политики, роли, временные периоды, доменные события.
// ❌ Невыразительная логика без имени
if (amount.compareTo(limit) <= 0 && !customer.isBlocked()) { /* ... */ }
// ✅ Политика — именованное правило
class CreditLimitPolicy {
boolean allows(Customer customer, Money amount) {
return customer.limit().remaining().isGreaterThan(amount);
}
}
// ✅ Период — доменная концепция вместо двух LocalDate
record BillingPeriod(LocalDate from, LocalDate to) {
boolean includes(LocalDate date) {
return !date.isBefore(from) && !date.isAfter(to);
}
}
// ✅ Событие — факт, а не побочный эффект
class OrderExpired implements DomainEvent { /* ... */ }
Явные типы упрощают тестирование и повторное использование. Давайте имя каждому важному правилу.
6. Гибкий дизайн (Supple Design)
Набор из шести принципов, которые делают доменную модель пластичной и точной. Это, пожалуй, самая недооценённая часть книги Эванса.
6.1 Intention-Revealing Interfaces
Публичный API говорит что происходит, а не как это сделано.
// ✅ Методы раскрывают намерение
class Order {
void confirm() { /* ... */ }
void cancelByCustomer(Reason reason) { /* ... */ }
void markAsPaid(PaymentId paymentId) { /* ... */ }
}
// ❌ Метод скрывает смысл
class Order {
void updateStatus(int code) { /* ... */ }
}
6.2 Side-Effect-Free Functions
Вычисления не должны менять состояние. Проще тестировать и переиспользовать.
// ✅ Чистое вычисление — не меняет ничего
class TaxCalculator {
Money taxFor(Order order) { /* pure calculation */ }
}
// ❌ Вычисление + побочный эффект в одном методе
class PricingService {
Money calculateAndApplyDiscount(Order order) { /* меняет order */ }
}
6.3 Assertions / Contracts
Инварианты проверяются там, где меняется состояние. Ошибки ловятся ближе к источнику.
// ✅ Инварианты на входе
class Account {
void withdraw(Money amount) {
if (amount.isNegative()) throw new IllegalArgumentException();
if (balance.isLessThan(amount)) throw new IllegalStateException();
balance = balance.subtract(amount);
}
}
// ❌ Молча позволяет невалидное состояние
class Account {
void withdraw(Money amount) {
balance = balance.subtract(amount); // отрицательный баланс? ок...
}
}
6.4 Conceptual Contours
Модель делится на устойчивые смысловые части. Изменения локализуются, меньше связей.
// ✅ Каждый контур — отдельная ответственность
interface PricingPolicy {
Money priceFor(Order order);
}
interface ShippingPolicy {
Money costFor(Shipment shipment);
}
// ❌ Все правила в одном месте
class OrderService {
void price() { }
void ship() { }
void invoice() { }
}
6.5 Declarative Design
Правило выражено как объект, а не как процедурная цепочка if/else. Можно переиспользовать и комбинировать.
// ✅ Правило — объект
class CanConfirmOrder implements Specification {
public boolean isSatisfiedBy(Order order) {
return order.isPaid() && order.hasItems();
}
}
// ❌ Правило — процедурная каша
if (paid && !items.isEmpty() && !isBlocked) { /* ... */ }
6.6 Closure of Operations
Операции возвращают объекты доменного языка, а не примитивы. Легче композиция.
// ✅ Money → Money (замкнутость)
Money addTax(Money base) {
return base.add(base.multiply(taxRate));
}
// ❌ double → double (теряем доменный тип)
double addTax(double base) {
return base + base * 0.2;
}
7. Аналитические паттерны
Повторяющиеся доменные структуры, которые помогают распознать форму домена. Это не рецепты, а отправные точки для разговора с экспертами.
// Party/Role — универсальная структура «участник + роль»
class Party {
private final PartyId id;
}
class CustomerRole {
private final Party party;
}
// Quantity — значение с единицей измерения
record Quantity(BigDecimal value, Unit unit) {
Quantity add(Quantity other) { /* проверка unit */ }
}
// Observation — измерение во времени
class Observation {
private final Instant time;
private final Quantity value;
}
// ❌ Паттерн применён без смысла
class PartyRole {
String type; /* используется как универсальный тип для всего */
}
Ключевые паттерны: Party/Role (участники и их роли), Accountability (ответственность), Observation (измерения), Quantity (значения с единицами). Используйте для разговора с экспертами, а не для слепого копирования.
8. Связь с паттернами проектирования (GoF)
Технические паттерны (Strategy, Factory, Specification) должны поддерживать модель домена, а не подменять её. Паттерн оправдан, если подчёркивает смысл домена.
// Strategy — для вариативного поведения домена
interface ShippingPolicy {
Money costFor(Shipment shipment);
}
class ExpressShipping implements ShippingPolicy {
public Money costFor(Shipment shipment) { /* ... */ }
}
// Factory — для создания с доменными правилами
class ShippingPolicyFactory {
ShippingPolicy forOrder(Order order) { /* выбор политики */ }
}
// ❌ Паттерн ради паттерна — Singleton без доменного смысла
class ConfigManager {
static ConfigManager INSTANCE = new ConfigManager();
}
Рефакторинг — не техническое упражнение, а инструмент углубления модели. Каждый рефакторинг должен приближать код к языку домена.
Итого: принципы проектирования
- Модель растёт из диалога — не фиксируйте её рано, постоянно уточняйте с экспертами
- Код = модель — анемичная модель с сервисами-контейнерами логики — антипаттерн
- Домен изолирован — зависимости направлены к домену, не от него
- Ищите прорывы — скрытые концепции, которые упрощают модель
- Делайте скрытое явным — политики, периоды, роли заслуживают собственных типов
- API раскрывает намерение —
confirm(), неupdateStatus(2) - Разделяйте вычисления и команды — чистые функции отдельно от изменений состояния
- Паттерны поддерживают модель — Strategy и Factory оправданы, когда подчёркивают домен
Эти принципы — фундамент, на котором строятся тактические, стратегические и интеграционные паттерны DDD.