Тактический дизайн в DDD: Мост между бизнесом и кодом

Тактический дизайн в DDD: Мост между бизнесом и кодом

Когда мы слышим Domain Driven Design, на ум сразу приходят такие понятия, как Ubiquitous Language (Повсеместный/Всеобщий язык), Bounded Contexts (Ограниченные контексты), то есть чаще всего говорят о стратегическом дизайне.

Но между стратегией и кодом лежит большой очень важный слой — тактический дизайн. Это то, как стратегические идеи воплощаются в конкретные и работоспособные программные модели. Тактический дизайн — инструмент моделирования Bounded Context.

Этой части DDD незаслуженно уделяют мало внимания, хотя без тактики любая стратегия терпит поражение.

Разница между стратегическим и тактическим дизайном

Стратегический дизайн — отвечает на вопросы «зачем» и «как» разделять:

🠶 Где границы между контекстами?

🠶 Какие модели и команды за что отвечают?

🠶 Как построено взаимодействие между контекстами?

Тактический дизайн — здесь находим ответы на вопросы «как» реализовать внутри одного ограниченного контекста (Bounded Context):

🠶 Какие сущности находятся внутри контекста?

🠶 Какие инварианты должны соблюдаться?

🠶 Где агрегаты и их границы?

Цели тактического дизайна

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

Основные задачи тактического дизайна:

🠶 Инкапсуляция логики — представление бизнес-логики в коде в максимально явном и читаемом виде.

🠶 Защита инвариантов — правила предметной области должны быть согласованы и непротиворечивы (простым языком, должна быть одна точка входа для изменения данных, которые живут в одной модели).

🠶 Обеспечить целостность данных.

🠶 Сделать код гибким, способным к быстрому изменению вслед за бизнес-логикой.

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

Пример тактического дизайна

В качестве примера рассмотрим «избитый» пример из контекста управления заказами.

Ниже — один из вариантов реализации управления заказом с применением некоторых паттернов тактического дизайна:

// Value Object: Важна цена и валюта. public record Money(BigDecimal amount, Currency currency) { public Money add(Money other) { /* проверка валюты и сложение */ } } // Entity: Позиция в заказе со своим идентификатором (id) class OrderItem { private OrderItemId id; private ProductId productId; private Money price; private Quantity quantity; // Создание новой позиции public static OrderItem create(ProductId productId, Money price, Quantity quantity) { return new OrderItem(OrderItemId.create(), productId, price, quantity); } // Логика расчета стоимости позиции public Money calculateSubtotal() { ... } } // Aggregate Root: Корень агрегата "Заказ". Единственная точка входа. class Order { private OrderId id; private CustomerId customerId; private OrderStatus status; private List<OrderItem> items; // Управляется только изнутри Order private Money totalAmount; // Важный инвариант: итоговая сумма должна быть равна сумме всех позиций. // Конструктор/фабрика и все методы его поддерживают. private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderItem::calculateSubtotal) .reduce(Money::add) .orElse(Money.ZERO); } // Основной метод. Инкапсулирует логику добавления. public void addItem(Product product, Quantity quantity) { // Проверка бизнес-правил: продукт в продаже? заказ открыт? if (this.status != OrderStatus.OPEN) { throw new IllegalStateException("Cannot modify closed order."); } var newLine = OrderItem.create(product.id(), product.price(), quantity); this.items.add(newLine); this.recalculateTotal(); // Поддержание инварианта this.registerDomainEvent(new ItemAddedEvent(this.id, newLine.id(), product.id())); } // Другие важные операции public void confirm() { ... } public void cancel() { ... } }

Что дает такая реализация:

  • Целостность - нельзя изменить OrderItem в обход Order.
  • Ясность - все правила управления заказом находятся в одном классе.
  • Безопасность - при изменении позиций заказа, инвариант totalAmount всегда будет пересчитан.

Почему не стоит упускать тактический дизайн и как это может повлиять на бизнес

Игнорирование тактического дизайна редко выглядит как проблема на старте. Если пропустить этот этап, то можно легко получить:

✕ "Анемичные" модели без поведения.

✕ Размазанную по всем компонентам (а иногда и сервисам) логику.

✕ Нарушения инвариантов - в разных местах кода могут производиться разные операции над инвариантами, а иногда и по разным правилам.

✕ Сложность в тестировании и поддержке.

В конечном итоге это может ударить по бизнесу:

✗ Потеря доверия клиентов из-за некорректных заказов.

✗ Дополнительные расходы на поддержку и исправление ошибок.

✗ Замедление скорости выхода новых фич.

✗ Невозможность быстро адаптироваться под новые правила ценообразования / акции / возвраты.

Хороший тактический дизайн - это страховка от дорогостоящих ошибок и технического долга.

Подпишись на мой телеграм-канал

1
Начать дискуссию