DDD — это настоящий спасательный круг для проектов со сложной бизнес-логикой. Подход, предложенный Эриком Эвансом, позволяет создавать элегантные решения, которые точно отражают реальную предметную область и могут эволюционировать вместе с ней. Java, со своей строгой типизацией и объектно-ориентированной природой, идеально подходит для воплощения принципов DDD. Однако дьявол, как обычно, кроется в деталях. Как правильно определить границы агрегатов? Какие из бесчисленных возможностей Java и её экосистемы выбрать для реализации репозиториев? Как организовать взаимодействие между ограничеными контекстами?
Эволюция проектирования программного обеспечения: от процедурного программирования до DDD
История проектирования софта напоминает эволюцию живых существ — каждый новый подход возникал как ответ на ограничения предыдущего. В начале были алгоритмы и процедурное программирование: простой линейный код, выполняющий последовательность операций. Этот примитивный подход работал для скромных задач, но стоило программам подрасти — и они превращались в неуправляемых монстров из спагетти-кода.
Объектно-ориентированное программирование стало настоящим прорывом, предложив инкапсуляцию, наследование и полиморфизм. Java, появившаяся в середине 90-х, сделала ООП мейнстримом. Но быстро обнаружилась новая проблема: даже с ООП разработчики часто создавали системы, где объекты превращались в анемичные модели данных, а вся логика уходила в сервисные классы. Технически код был объектно-ориентированным, но концептуально — процедурным. Появление паттернов проектирования помогло структурировать мышление разработчиков, но не решило фундаметальную проблему: разрыв между моделью предметной области и программным кодом. Программисты говорили на языке технологий, а эксперты предметной области — на языке бизнеса. Никто не понимал друг друга. MVC, MVP, MVVM и другие архитектурные паттерны пытались навести порядок в слоях приложения, однако эти подходы больше касались презентационного уровня и слабо затрагивали суть самого домена.
DDD возник как следуйщий шаг эволюции, предложив революцыонный на тот момент подход: моделировать програмное обеспечение вокруг концепций предметной области, а не технических аспектов. Вместо того чтобы подгонять бизнес-логику под возможности фреймворков и баз данных, DDD предложил поставить в центр саму предметную облась. Java, с её выразительным синтаксисом и сильной экосистемой, стала идеальной платформой для воплощения идей DDD в жизнь.
Как правильно приготовить DDD (domain-driven design) Достался проект, который изначально планировался как DDD, но ребята которые его делали до меня... JUnit, данные из XML, Data Driven Testing Пытаюсь организовать data-driven test (DDT) на JUnit c взятием тестовых данных из XML-файла. Всё... JUnit, данные из XML, Data Driven Testing Пытаюсь организовать data-driven test (DDT) на JUnit c взятием тестовых данных из XML-файла. Всё... Ищу книгу: Java Programming: Program Design Including Data Structures Весь интернет обыскала в поисках этой книги:
Java Programming: Program Design Including Data...
Краткий обзор DDD как подхода к разработке сложных систем с помощью Java
Domain-Driven Design (DDD) — это подход к разработке программного обеспечения, который заставляет нас вначале думать, а потом уже писать код. Звучит слишком очевидно? Однако в индустрии, где часто преобладает принцип "кодируй скорее, думай потом", такой подход выглядит почти революционно.
Суть DDD в том, что сложные проблемы требуют глубокого понимания предметной области. Вместо того, чтобы тонуть в запутаных дебрях абстрактных классов и избыточных интерфейсов, DDD предлагает создать модель, которая соответсвует реальным бизнес-процессам. И вот здесь на сцену выходит Java с её богатым объектно-ориентированным функционалом, который словно создан для воплощения идей DDD. Основная ценность этого подхода проявляется, когда бизнес-логика ветвится, усложняется и мутирует быстрее, чем вы успеваете нажимать Ctrl+S. В таких случаях DDD предлагает чёткую структуру для моделирования:
1. Единый язык (Ubiquitous Language) — тот самый "мост" между технарями и бизнесменами, позволяющий говорить об одних и тех же вещях одними терминами.
2. Ограниченные контексты (Bounded Contexts) — четкие границы, внутри которых конкретные термины имеют конкретный смысл. В одном контексте "Счёт" может означать банковский аккаунт, в другом — электронный документ для оплаты.
3. Предметно-ориентированные сущности — классы Java, отражающие важные для бизнеса объекты с уникальной идентичностью (например, Customer, Order).
4. Объекты-значения — иммутабельные объекты без идентичности, описывающие характеристики (например, Money, Address).
В Java эти концепции воплощаются удивительно гармонично. Классы с полями и методами прекрасно представляют сущности и их поведение. Иммутабельные классы (особенно с приходом records в Java 16) идеально подходят для объектов-значений. Интерфейсы помогают определять контракты между слоями и компонентами.
Когда я начал применять DDD на реальных проектах, мне открылась неожиданная истина — это не столько техническая методология, сколько способ мышления. Я перестал думать о "таблицах в базе" и "передаче данных между слоями", вместо этого я стал думать в термининах бизнес-процессов и взаимодействий между бизнес-объектами. Это требует определенной перестройки сознания, но результат стоит усилий.
Когда стоит применять DDD: критерии выбора и признаки подходящих проектов
Domain-Driven Design — мощный инструмент, но, как и любая методология, он не является универсальным решением для всех проблем. Прежде чем погружаться в пучину DDD с головой, стоит трезво оценить, подходит ли этот подход для вашего конкретного проекта. DDD приносит наибольшую пользу, когда предметная область — сложная и запутаная. Если вы разрабатываете CRUD-приложение с простой логикой или типовой интернет-магазин — скорее всего, вы потратите больше времени на организацию DDD-структуры, чем выиграете от её применения. В таких случаях более традиционная трехслойная архитектура будет эффективнее. Вот несколько признаков того, что ваш проект может стать идеальным кандидатом для применения DDD:
1. Сложная бизнес-логика. Когда правила предметной области похожи на запутанный клубок с множеством условий, исключений и зависимостей — DDD помогает структурировать этот хаос.
2. Проект с долгой жизнью. Если вы пишете не одноразовый MVP, а систему, которая будет развиваться годами, инвестиции в DDD окупятся сторицей.
3. Команда включает экспертов предметной области. DDD требует постояного взаимодействия с людьми, которые глубоко понимают бизнес-процессы. Без них модель станет просто вашей интерпретацией, которая может сильно расходится с реальностью.
4. Размер команды. В больших командах, где разные группы работают над разными частями системы, DDD помогает устанавливать чёткие границы ответственнности через ограниченные контексты.
5. Эволюционирующие требования. Когда требования постоянно меняются и уточняются, гибкая предметно-ориентированная модель позволяет адаптироваться без полного переписывания системы.
В моей практике был показательный случай: мы взялись за переписывание системы управления медицинским центром, которая обросла слоями наследованого кода за 7 лет. Применив DDD, мы смогли выделить четкие ограниченные контексты (запись пациентов, медицинские карты, биллинг) и создать ядро, которое точно отражало бизнес-процессы клиники. Результат — система, которая легко адаптируется под меняющиеся требования регуляторов и новые бизнес-сценарии.
Основные концепции DDD
Чтобы по-настоящему понять DDD, нужно погрузиться в его ключевые концепции. За годы работы с этой методологией я убедился: именно глубокое понимание основных "строительных блоков" DDD делает разницу между посредственным и выдающимся дизайном. Рассмотрим наиболее важные из них, которые превращают DDD из теории в практику.
Единый язык и ограниченные контексты
Единый язык (Ubiquitous Language) — пожалуй, самая недооценённая, но при этом революционная идея DDD. Это общий язык, на котором говорят как программисты, так и эксперты предметной области. Представьте ситуацию: разработчики называют нечто "юзером", бизнес говорит "клиент", в документации фигурирует "покупатель", а в базе данных это "cstmr". Вавилонское столпотворение! Единый язык устраняет эту путаницу. Важно понять — единый язык это не просто глоссарий терминов. Это активный инструмент моделирования, который эволюционирует вместе с пониманием домена. Когда вы называете класс в Java именно так, как соответствующую концепцию называют эксперты домена, вы создаете модель, понятную всем участникам проекта.
| Java | 1
2
3
4
5
6
7
8
9
10
11
| // Плохо: технический жаргон
public class UserDTO {
private String usrName;
private List<PurchItm> items;
}
// Хорошо: единый язык
public class Customer {
private CustomerName name;
private List<OrderItem> purchasedItems;
} |
|
Ограниченные контексты (Bounded Contexts) — это ответ на реальность больших систем, где одно и то же понятие может иметь разный смысл в разных частях приложения. Например, "Заказ" в контексте продаж и "Заказ" в контексте доставки — совершенно разные сущности с разным набором атрибутов и поведением. В коде ограниченые контексты часто реализуются через модульную структуру проекта:
| Java | 1
2
3
4
5
6
7
8
9
| com.example.ecommerce
├── sales
│ └── domain
│ ├── Order.java (содержит цены, скидки, статусы оплаты)
│ └── ...
└── shipping
└── domain
├── Order.java (содержит адреса, сроки, службу доставки)
└── ... |
|
Сущности, агрегаты и объекты-значения
Сущности (Entities) — обьекты домена, имеющие уникальную идентичность, которая сохраняется независимо от изменений их атрибутов. В Java сущности обычно реализуются как классы с методами equals() и hashCode(), сравнивающими объекты по ID, а не по содержимому.
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class Product {
private final ProductId id;
private String name;
private Money price;
// Сравнение только по идентификатору
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return id.equals(product.id);
}
} |
|
Объекты-значения (Value Objects) — иммутабельные объекты, которые характеризуются совокупностью своих атрибутов и не имеют идентификатора. Классический пример — денежная сумма. Десять долларов всегда десять долларов, неважно, какой экземпляр класса Money их представляет.
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public final class Money {
private final BigDecimal amount;
private final Currency currency;
// Конструктор, валидирующий входные параметры
public Money(BigDecimal amount, Currency currency) {
if (amount == null) throw new IllegalArgumentException("Amount cannot be null");
if (currency == null) throw new IllegalArgumentException("Currency cannot be null");
this.amount = amount;
this.currency = currency;
}
// Бизнес-операции, возвращающие новые экземпляры
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// Здесь equals/hashCode сравнивают по всем полям
} |
|
Агрегаты (Aggregates) — кластеры связаных объектов, которые рассматриваются как единое целое с точки зрения изменения данных. Каждый агрегат имеет корень (Aggregate Root) — сущность, через которую осуществляется доступ ко всем внутренним объектам агрегата. Это основополагающая идея в DDD, обеспечивающая консистентность данных:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class Order { // Корень агрегата
private final OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines; // Часть агрегата
private OrderStatus status;
// Бизнес-метод, управляющий внутренними частями агрегата
public void addProduct(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new OrderAlreadyConfirmedException("Cannot modify confirmed order");
}
// Инкапсуляция изменений внутри агрегата
OrderLine existingLine = findOrderLineByProduct(product);
if (existingLine != null) {
existingLine.increaseQuantity(quantity);
} else {
orderLines.add(new OrderLine(product, quantity));
}
}
} |
|
Репозитории и сервисы предметной области
Репозитории (Repositories) обеспечивают доступ к агрегатам, скрывая детали хранения данных. Они создают иллюзию коллекции объектов в памяти, хотя фактически могут извлекать данные из БД, API или других источников.
| Java | 1
2
3
4
5
6
| public interface OrderRepository {
Order findById(OrderId id);
List<Order> findByCustomer(CustomerId customerId);
void save(Order order);
void remove(Order order);
} |
|
Сервисы предметной области (Domain Services) инкапсулируют бизнес-логику, которая не вписывается естественным образом ни в одну сущность или объект-значение. Например, если операция затрагивает несколько агрегатов или требует взаимодействия с внешним сервисом:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public class OrderProcessingService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
public void processOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId);
// Проверка наличия товаров на складе
boolean allAvailable = order.getOrderLines().stream()
.allMatch(line -> inventoryService.isAvailable(line.getProductId(), line.getQuantity()));
if (!allAvailable) {
throw new ProductNotAvailableException("Some products are out of stock");
}
// Обработка платежа
PaymentResult result = paymentService.processPayment(order);
if (result.isSuccessful()) {
order.confirm();
orderRepository.save(order);
} else {
order.markPaymentFailed(result.getErrorMessage());
orderRepository.save(order);
}
}
} |
|
Эти концепции формируют прочный фундамент для построения моделей предметной области. Когда я начинаю проектировать новую систему с использованием DDD, я стараюсь сначала выявить основные сущьности и их взаимоотношения, затем сгруппировать их в агрегаты, и в конце определить сервисы домена для операций, которые выходят за рамки отдельных агрегатов.
Объекты-значения и сущности в DDD не всегда легко разграничить. Я частенько спотыкался об эту грань, пока не сформулировал для себя простой критерий: если два обьекта с одинаковыми атрибутами взаимозаменяемы — перед нами объект-значение, если нет — сущность.
Фабрики в DDD
Создание сложных обьектов домена — это отдельный вопрос, который обычно решают с помощью фабрик. Фабрики инкапсулируют логику конструирования, скрывая детали и обеспечивая создание корректных экземпляров объектов:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class OrderFactory {
private final ProductCatalog productCatalog;
public Order createOrder(CustomerId customerId, Map<ProductId, Integer> items) {
Order order = new Order(OrderId.generate(), customerId);
for (Map.Entry<ProductId, Integer> entry : items.entrySet()) {
Product product = productCatalog.getProduct(entry.getKey());
if (product == null) {
throw new ProductNotFoundException("Product not found: " + entry.getKey());
}
order.addProduct(product, entry.getValue());
}
return order;
}
} |
|
Такая фабрика не просто создает объект — она проверяет бизнес-правила и обеспечивает создание валидного агрегата.
События предметной области
События предметной области (Domain Events) — еще одна мощная концепция DDD. Событие фиксирует факт, что что-то значимое для домена произошло. Например, заказ был размещен, платеж подтвержден или товар отправлен.
| Java | 1
2
3
4
5
6
7
8
| public class OrderPlacedEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final LocalDateTime timestamp;
private final List<OrderLineItem> items;
// Конструктор, геттеры...
} |
|
События служат нескольким целям:
Фиксация изменений в домене
Асинхронное взаимодействие между ограниченными контекстами
Аудит и отслеживание бизнес-процессов
Практическая реализация событий может быть разной — от простого паттерна Observer до полноценной системы Event Sourcing. В простейшем случае в Java можно использовать механизм ApplicationEventPublisher из Spring Framework:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @Service
public class OrderService {
private final OrderRepository repository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void placeOrder(Order order) {
repository.save(order);
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(),
order.getCustomerId(),
LocalDateTime.now(),
order.getOrderLines()
);
eventPublisher.publishEvent(event);
}
} |
|
Спецификации
Спецификации (Specifications) — это объекты, которые инкапсулируют бизнес-правила и условия. Они отвечают на вопрос: "Соответствует ли объект определённым критериям?". В Java спецификации часто реализуются через предикаты:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public interface OrderSpecification {
boolean isSatisfiedBy(Order order);
}
public class HighValueOrderSpecification implements OrderSpecification {
private final Money threshold;
public HighValueOrderSpecification(Money threshold) {
this.threshold = threshold;
}
@Override
public boolean isSatisfiedBy(Order order) {
return order.getTotalAmount().isGreaterThanOrEqual(threshold);
}
} |
|
Спецификации можно комбинировать, создавая сложные правила из простых компонентов:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class AndSpecification implements OrderSpecification {
private final OrderSpecification left;
private final OrderSpecification right;
public AndSpecification(OrderSpecification left, OrderSpecification right) {
this.left = left;
this.right = right;
}
@Override
public boolean isSatisfiedBy(Order order) {
return left.isSatisfiedBy(order) && right.isSatisfiedBy(order);
}
} |
|
В моей практике правильно обозначенные и организованные основные концепции DDD дают солидный фундамент для создания гибкой и поддерживаемой архитектуры. Они позволяют говорить о программе на языке бизнеса, что значительно облегчает коммуникацию с заказчиками и экспертами домена.
Стратегический дизайн: карта контекстов и взаимодействие между ограниченными контекстами
Если тактический DDD фокусируется на деталях конкретной модели, то стратегический DDD поднимает взгляд до уровня птичьего полёта — как разные части сложной системы взаимодействуют между собой. Именно здесь на сцену выходит инструмент карты контекстов (Context Map) — визуальное представление всех ограниченных контекстов системы и их взаимоотношений. Представьте карту контекстов как архитектурный чертёж здания, где каждая комната — это отдельный ограниченный контекст со своим особым назначением. Коридоры и двери между ними — это формы взаимодействия между контекстами.
В реальных проектах эти взаимоотношения принимают различные формы, каждая со своим назначением:
1. Общее ядро (Shared Kernel) — когда несколько контекстов используют общий код. Это как общая стена между квартирами — экономит ресурсы, но требует согласования при любых изменениях.
| Java | 1
2
3
4
5
6
7
8
9
| // Библиотека, используемая несколькими контекстами
package com.example.sharedkernel;
public final class Money {
private final BigDecimal amount;
private final Currency currency;
// Методы, используемые во всех контекстах
} |
|
2. Заказчик-поставщик (Customer-Supplier) — асимметричные отношения, где один контекст (заказчик) имеет приоритет в определении требований к API другого контекста (поставщика).
3. Конформист (Conformist) — когда зависимый контекст вынужден принимать модель доминирующего контекста без возможности влиять на неё. Часто встречается при интеграции с крупными внешними системами.
4. Антикоррупционный слой (Anticorruption Layer) — пожалуй, самый мощный защитный механизм, который позволяет изолировать модель от внешних влияний:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class LegacySystemAdapter implements ModernServiceInterface {
private final LegacySystem legacySystem;
@Override
public CustomerData getCustomerInfo(CustomerId id) {
// Трансформация между моделями
LegacyCustomerRecord record = legacySystem.findCustomerById(id.toString());
return new CustomerData(
new CustomerId(record.getId()),
new CustomerName(record.getFirstName(), record.getLastName()),
transformAddress(record.getAddressData())
);
}
} |
|
5. Служба с открытым протоколом (Open Host Service) — контекст, предоставляющий хорошо документированное API для множества потребителей.
Я сталкивался с проектом, где игнорирование чётких границ между контекстами привело к тому, что система превратилась в мифическое чудовище — каждое изменение в одной части вызывало неожиданные последствия в другой. Нам пришлось потратить месяцы на то, чтобы разделить её на ограниченные контексты с чётко проработанными интерфейсами.
Карта контекстов должна отражать не только технические аспекты, но и организационные реалии. Она должна учитывать, какие команды разрабатывают какие части системы, как устроены процессы координации между ними. Как показывает закон Конвея, архитектура системы неизбежно повторяет коммуникационную структуру организации. При создании карты контекстов стоит начинать с наиболее важных бизнес-возможностей и постепено двигаться к деталям. В противном случае вы рискуете утонуть в мелочах, упустив из виду большую картину.
События предметной области и целостность агрегатов
События предметной области (Domain Events) — это наблюдаемые факты, происходящие в системе, которые имеют значение для экспертов домена. Они фиксируют "что произошло" в отличие от команд, выражающих намерение "что должно произойти". Помню свой первый проект с применением событий — это было откровение, когда мы смогли выстроить ясную хронологию действий в сложной бизнес-процедуре обработки страховых претензий.
События предметной области обычно именуются в прошедшем времени: OrderPlaced, PaymentReceived, ShipmentDelivered. Это не просто соглашение об именовании — оно подчеркивает суть события как свершившегося факта и упрощает понимание системы всеми заинтересованными сторонами.
В Java события можно реализовать как простые POJO-объекты:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class PaymentReceivedEvent {
private final OrderId orderId;
private final PaymentId paymentId;
private final Money amount;
private final LocalDateTime occurredOn;
public PaymentReceivedEvent(OrderId orderId, PaymentId paymentId, Money amount) {
this.orderId = orderId;
this.paymentId = paymentId;
this.amount = amount;
this.occurredOn = LocalDateTime.now();
}
// Геттеры, но не сеттеры - события иммутабельны
} |
|
Ключевая особенность событий — иммутабельность. Событие фиксирует факт, произошедший в определённый момент времени, и этот факт не может быть изменён. События часто служат основой для временной согласованности в распределённых системах, где немедленная консистентность не всегда достижима.
Публикация и прослушивание событий может быть реализована с помощью специализированных библиотек вроде Axon Framework, простым Event Bus или средствами Spring Framework:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @Service
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
orderRepository.save(order);
// Публикация события
eventPublisher.publishEvent(new OrderConfirmedEvent(orderId));
}
}
@Component
public class InventoryHandler {
@EventListener
public void handleOrderConfirmed(OrderConfirmedEvent event) {
// Резервирование товаров на складе
}
} |
|
Одна из самых сложных задач при работе с агрегатами — обеспечение их целостности. Ведь агрегат, по определению, должен быть консистентной единицей — все его внутренние части должны находиться в согласованном состоянии после любой операции. В монолитных приложениях этого достаточно легко достичь с помощью транзакций базы данных. Однако в распределённых и микросервисных архитектурах дело усложняется. Разные агрегаты могут лежать в разных базах данных или даже управлятся разными сервисами. Тут на сцену выходят несколько стратегий:
1. Конечная согласованность (Eventual Consistency). Мы признаём, что система не может всегда быть полностью консистентной, но стремимся к тому, чтобы все изменения рано или поздно распространились. События домена идеально подходят для этого подхода.
2. Саги (Sagas). Длительные бизнес-процессы, состоящие из множества шагов, каждый из которых изменяет отдельный агрегат. Важная часть саг — компенсирующие действия, которые откатывают изменения при сбоях:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| @Service
public class OrderProcessingSaga {
private final OrderService orderService;
private final PaymentService paymentService;
private final ShippingService shippingService;
@Transactional
public void process(OrderId orderId) {
try {
// Шаг 1: Подтверждение заказа
orderService.confirmOrder(orderId);
// Шаг 2: Обработка платежа
PaymentId paymentId = paymentService.processPayment(orderId);
// Шаг 3: Организация доставки
shippingService.arrangeShipment(orderId);
} catch (PaymentFailedException e) {
// Компенсирующее действие при сбое платежа
orderService.rejectOrder(orderId, "Payment failed: " + e.getMessage());
} catch (ShippingException e) {
// Компенсация при проблеме с доставкой
paymentService.refundPayment(orderId);
orderService.rejectOrder(orderId, "Shipping failed: " + e.getMessage());
}
}
} |
|
3. Версионирование агрегатов помогает обнаруживать конфликтующие изменения — особенно полезно в системах, где несколько пользователей могут одновременно работать с одним и тем же агрегатом:
| Java | 1
2
3
4
5
6
7
8
9
10
| @Entity
public class Order {
@Id
private OrderId id;
@Version
private Long version;
// Другие поля
} |
|
Когда JPA обнаруживает конфликтное изменение, оно выбрасывает OptimisticLockException. Это позволяет обрабатывать конфликты на уровне приложения — например, показывая пользователю информативное сообщение или пытаясь автоматически разрешить конфликт.
4. CQRS (Command Query Responsibility Segregation) — разделение операций чтения и записи. Это позволяет оптимизировать каждую сторону отдельно и использовать разные модели данных для них.
В моей практике наиболее удачным оказался комбинированый подход: использование событий домена для обеспечения конечной согласованности между сервисами и оптимистической блокировки для защиты от конфликтных изменений внутри одного агрегата. Я заметил интересную закономерность: чем лучше мы определяем границы агрегатов, тем проще обеспечивать их целостность. Часто желание создать слишком большой агрегат приводит к проблемам с производительностью и конкуренцией за ресурсы. А слишком мелкие агрегаты создают ненужную связность между собой. Золотая середина — выделение агрегатов, которые действительно представляют атомарные единицы с точки зрения бизнес-правил.
Фабрики и тактические паттерны DDD
Как разработчики, мы часто погружаемся в детали реализации, забывая о более широкой картине. Тактические паттерны DDD — это строительные блоки, позволяющие нам воплотить стратегическую канву в конкретные строчки кода. И хотя мы уже затронули некоторые из них, есть еще несколько мощных инструментов, заслуживающих внимания.
Фабрики — один из наиболее недооцененных паттернов. Я часто вижу, как разработчики перегружают конструкторы сложной логикой или, еще хуже, создают анемичные обьекты, а потом вызывают десяток сеттеров. Фабрики решают эту проблему элегантно — они инкапсулируют сложную логику создания, обеспечивая появление только валидных объектов:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class UserFactory {
private final PasswordEncoder encoder;
private final IdGenerator idGenerator;
public User createUser(String email, String plainPassword, String firstName, String lastName) {
if (!EmailValidator.isValid(email)) {
throw new InvalidEmailException(email);
}
UserId id = idGenerator.nextId();
String hashedPassword = encoder.encode(plainPassword);
return new User(id, new Email(email), hashedPassword,
new PersonName(firstName, lastName), UserStatus.ACTIVE);
}
public User reactivateDeactivatedUser(User deactivatedUser) {
if (deactivatedUser.getStatus() != UserStatus.DEACTIVATED) {
throw new IllegalUserStateException("Can only reactivate deactivated users");
}
// Клонирование пользователя с новым статусом
return new User(deactivatedUser.getId(),
deactivatedUser.getEmail(),
deactivatedUser.getPassword(),
deactivatedUser.getName(),
UserStatus.ACTIVE);
}
} |
|
А что насчет модулей? В Java модули — это естественный способ организовать ограниченые контексты. Каждый модуль становиться самодостаточной единицей с четко определеной ответственностью:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| com.example.ecommerce
├── catalog
│ ├── domain
│ │ ├── Product.java
│ │ ├── ProductRepository.java
│ │ └── ...
│ ├── application
│ │ ├── ProductSearchService.java
│ │ └── ...
│ └── infrastructure
│ ├── JpaProductRepository.java
│ └── ...
├── ordering
│ ├── domain
│ │ ├── Order.java
│ │ └── ... |
|
Политики — еще один мощный инструмент, особенно когда речь идет о сложных бизнес-правилах. Я сам попал в ловушку, пытаясь втиснуть правила расчета скидок непосредственно в сущность Order. Код превратился в запутанное месиво условий. Выделение отдельных политик навело порядок:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public interface DiscountPolicy {
Money calculateDiscount(Order order);
}
public class LoyaltyDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscount(Order order) {
Customer customer = order.getCustomer();
if (customer.getLoyaltyLevel() == LoyaltyLevel.GOLD) {
return order.getTotalAmount().multiply(0.1); // 10% скидка для Gold-клиентов
}
return Money.ZERO;
}
} |
|
В реальной практике меня неоднократно выручал паттерн "Спецификация", который частенько забывают в контексте DDD. Он позволяет создавать переиспользуемые предикаты для проверки бизнес-правил:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public interface Specification<T> {
boolean isSatisfiedBy(T entity);
}
public class HighValueOrderSpecification implements Specification<Order> {
private final Money threshold;
// Конструктор, методы...
@Override
public boolean isSatisfiedBy(Order order) {
return order.getTotalAmount().isGreaterThan(threshold);
}
} |
|
Умелое сочетание этих тактических паттернов делает код не только более структурированым и поддерживаемым, но и ближе к языку предметной области, что и является главной целью DDD.
Практическая реализация в Java
После погружения в теорию пора закатать рукава и заняться конкретной реализацией DDD в Java-проектах. Когда я только начинал применять DDD в реальных проектах, меня буквально пришибло озарение — Java со своей строгой типизацией и богатой экосистемой словно создана для этой методологии!
Структура проекта и организация кода
Первое, с чем сталкивается разработчик, — это вопрос "как организовать код?" В мире DDD структура проекта должна отражать модель предметной области, а не технические детали. Вот схема, которая доказала свою эффективность в моих проектах:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| com.example.application
├── domain // Ядро домена - наша сокровищница
│ ├── model // Классы модели домена
│ │ ├── order
│ │ │ ├── Order.java
│ │ │ ├── OrderLine.java
│ │ │ └── OrderStatus.java
│ │ └── customer
│ │ └── ...
│ ├── service // Сервисы предметной области
│ └── repository // Интерфейсы репозиториев
├── application // Прикладной уровень - оркестровка
│ ├── service // Прикладные сервисы
│ ├── dto // Объекты передачи данных
│ └── mapper // Преобразователи между DTO и доменом
└── infrastructure // Инфраструктурный уровень
├── persistence // Реализация хранения данных
│ ├── repository // Конкретные реализации репозиториев
│ └── entity // Маппинг на сущности JPA (если используется)
├── security // Безопасность
└── integration // Интеграция с внешними системами |
|
Эта структура отражает слоистую архитектуру DDD, где домен изолирован от внешних механизмов и фреймворков. Зависимости направлены внутрь — инфраструктура зависит от домена, а не наоборот.
Реализация доменных объектов
Рассмотрим практическую реализацию доменных объектов в Java. Начнем с объекта-значения:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| public final class CustomerName {
private final String firstName;
private final String lastName;
public CustomerName(String firstName, String lastName) {
// Валидация и защита инвариантов
if (firstName == null || firstName.trim().isEmpty()) {
throw new IllegalArgumentException("First name cannot be empty");
}
if (lastName == null || lastName.trim().isEmpty()) {
throw new IllegalArgumentException("Last name cannot be empty");
}
this.firstName = firstName;
this.lastName = lastName;
}
// Бизнес-методы
public String getFullName() {
return firstName + " " + lastName;
}
// Equals и hashCode на основе всех полей
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerName that = (CustomerName) o;
return firstName.equals(that.firstName) &&
lastName.equals(that.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
} |
|
Обратите внимание на ключевые характеристики:- Иммутабельность (final класс, final поля, нет сеттеров).
- Валидация в конструкторе для защиты инвариантов.
- Реализация equals/hashCode на основе всех полей.
Теперь взглянем на пример реализации сущности и корня агрегата:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| public class Order {
private final OrderId id; // Идентификатор - Value Object
private CustomerId customerId;
private final List<OrderLine> orderLines = new ArrayList<>();
private OrderStatus status = OrderStatus.DRAFT;
private DeliveryAddress deliveryAddress;
protected Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
}
// Бизнес-метод, изменяющий состояние агрегата
public void addProduct(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new OrderAlreadyConfirmedException(
"Cannot modify order in status: " + status);
}
Optional<OrderLine> existingLine = findLineByProduct(product.getId());
if (existingLine.isPresent()) {
existingLine.get().increaseQuantity(quantity);
} else {
orderLines.add(new OrderLine(new OrderLineId(), product.getId(),
product.getPrice(), quantity));
}
}
// Другие бизнес-методы
public void confirm() {
if (orderLines.isEmpty()) {
throw new EmptyOrderException("Cannot confirm an empty order");
}
if (deliveryAddress == null) {
throw new MissingDeliveryAddressException("Delivery address is required");
}
this.status = OrderStatus.CONFIRMED;
}
// Геттеры, но не сеттеры для полей, изменяемых только через бизнес-методы
} |
|
Ключевые особенности этой реализации:- Защита инвариантов через бизнес-методы, а не через прямой доступ к полям.
- Инкапсуляция логики изменения состояния.
- Выбрасывание доменных исключений при нарушении бизнес-правил.
- Ограниченный конструктор (protected) для использования с фабрикой.
Взаимодействие с базами данных
Один из самых болезненных вопросов в DDD — это сохранение богатой доменной модели в реляционную базу данных. Проблема известна как "object-relational impedance mismatch" (несоответствие объектной и реляционной моделей). В Java обычно используется несколько подходов:
1. Активное использование JPA с ограниченым маппингом — доменные объекты содержат минимальный набор аннотаций JPA, а часть логики маппинга выносится в инфраструктурный слой.
2. Полное разделение — доменные объекты вообще ничего не знают о персистентности, а инфраструктурный слой содержит отдельные классы сущностей JPA и преобразователи.
В моей практике лучше всего себя зарекомендовал второй подход, хотя он требует больше кода. Зато он полностью защищает домен от "просачивания" деталей инфраструктуры и облегчает тестирование.
Вот пример разделения домена и инфраструктуры персистентности:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Domain - доменная модель ничего не знает о JPA
public class Order {
private final OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines;
// ... бизнес-методы
}
// Infrastructure - JPA-сущность для хранения
@Entity
@Table(name = "orders")
public class OrderEntity {
@EmbeddedId
private OrderIdJpaEmbeddable id;
@Column(name = "customer_id")
private String customerId;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "order_id")
private List<OrderLineEntity> orderLines;
// ... геттеры и сеттеры
}
// Infrastructure - маппер между доменом и JPA
public class OrderMapper {
public OrderEntity toEntity(Order order) {
// Преобразование из доменной модели в JPA-сущность
}
public Order toDomain(OrderEntity entity) {
// Преобразование из JPA-сущности в доменную модель
}
} |
|
Реализация репозитория при таком подходе выглядит так:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderMapper mapper;
@Override
public Order findById(OrderId id) {
return jpaRepository.findById(mapper.toJpaId(id))
.map(mapper::toDomain)
.orElseThrow(() -> new OrderNotFoundException(id));
}
@Override
public void save(Order order) {
OrderEntity entity = mapper.toEntity(order);
jpaRepository.save(entity);
}
} |
|
Этот подход имеет свою цену — больше кода и дополнительный слой маппинга. Но выигрыш в чистоте домена и облегчении тестирования значительно перевешивают эти недостатки.
Интеграция с современными Java-фреймворками
Интеграция DDD с современными фреймворками вроде Spring требует определенного подхода. Вот несколько проверенных практик:
1. Используйте Spring Boot для конфигурации, но не впускайте аннотации вроде @Autowired в домен.
2. Применяйте паттерн "Порты и адаптеры" для изоляции домена:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Порт в доменном слое
public interface PaymentGateway {
PaymentResult processPayment(PaymentDetails details);
}
// Адаптер в инфраструктурном слое
@Service
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult processPayment(PaymentDetails details) {
// Адаптация доменного запроса к API Stripe
StripePaymentRequest request = mapToStripeRequest(details);
StripePaymentResponse response = stripeClient.charge(request);
return mapToPaymentResult(response);
}
} |
|
3. Разделяйте API-модели и доменные модели. API должно отражать потребности клиентов вашего сервиса, а не экспонировать внутренюю структуру домена:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService service;
@PostMapping
public OrderResponse createOrder(@RequestBody CreateOrderRequest request) {
// Преобразование API-запроса в команду домена
CreateOrderCommand command = mapToCommand(request);
OrderId orderId = service.createOrder(command);
// Возврат упрощённого ответа, а не доменной модели
return new OrderResponse(orderId.getValue());
}
} |
|
Этот подход позволяет использовать всю мощь современных фреймворков, не жертвуя чистотой доменной модели.
В моей практике именно эти технические детали реализации часто определяли успех или провал внедрения DDD. Неправильное пересечение слоев или злоупотребление аннотациями фреймворка в домене может сделать всю архитектуру хрупкой и сложной для изменений.
Модульные тесты и использование Java Records
Тестирование — краеугольный камень надежных DDD-систем. Правильно спроектированные доменные объекты с чёткими границами агрегатов значительно упрощают написание тестов. В своей практике я часто следую подходу, при котором тесты пишутся на трёх уровнях:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| public class OrderTest {
@Test
void shouldCreateOrderWithValidData() {
// Arrange
CustomerId customerId = new CustomerId("C12345");
OrderId orderId = new OrderId("O98765");
// Act
Order order = new Order(orderId, customerId);
// Assert
assertEquals(customerId, order.getCustomerId());
assertEquals(OrderStatus.DRAFT, order.getStatus());
}
@Test
void shouldThrowExceptionWhenConfirmingEmptyOrder() {
// Arrange
Order order = createEmptyOrder();
// Act & Assert
assertThrows(EmptyOrderException.class, () -> order.confirm());
}
@Test
void shouldChangeStatusToConfirmedWhenOrderIsValid() {
// Arrange
Order order = createValidOrder();
// Act
order.confirm();
// Assert
assertEquals(OrderStatus.CONFIRMED, order.getStatus());
}
} |
|
Ценность таких тестов невозможно переоценить — они фиксируют бизнес-правила и обеспечивают стабильность модели при рефакторинге. Учитывая, что в DDD бизнес-логика концентрируется в доменной модели, хорошее покрытие тестами этого слоя является приоритетом.
Особенно важно тестировать инварианты — те условия, которые должны всегда выполнятся для доменного объекта. Вот пример теста, проверяющего инвариант "заказ не может быть подтверждён без адреса доставки":
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Test
void shouldNotAllowConfirmationWithoutDeliveryAddress() {
// Arrange
Order order = OrderTestFactory.createOrderWithItems();
// Намеренно не устанавливаем адрес доставки
// Act & Assert
MissingDeliveryAddressException exception = assertThrows(
MissingDeliveryAddressException.class,
() -> order.confirm()
);
assertThat(exception.getMessage()).contains("Delivery address is required");
} |
|
JUnit 5 с его расширенными ассертами и мощной поддержкой параметризаваных тестов особенно хорош для тестирования домена. Параметризованые тесты позволяют проверить множество сценариев без дублирования кода:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @ParameterizedTest
@MethodSource("provideInvalidMoneyAmounts")
void shouldRejectInvalidMoneyAmounts(BigDecimal amount, Currency currency, String expectedErrorMessage) {
// Act & Assert
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new Money(amount, currency)
);
assertThat(exception.getMessage()).contains(expectedErrorMessage);
}
static Stream<Arguments> provideInvalidMoneyAmounts() {
return Stream.of(
Arguments.of(null, Currency.getInstance("USD"), "Amount cannot be null"),
Arguments.of(BigDecimal.valueOf(-10), Currency.getInstance("USD"), "Amount cannot be negative"),
Arguments.of(BigDecimal.valueOf(10), null, "Currency cannot be null")
);
} |
|
В моем последнем проекте мы даже пошли дальше: написали собственные assertions, специфичные для нашего домена. Это сделало тесты невероятно вразительными:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Test
void shouldCalculateCorrectOrderTotal() {
// Arrange
Order order = createOrderWithMultipleItems();
// Act
Money total = order.calculateTotal();
// Assert
assertThat(total).isEqualToMoney("120.50", "USD");
assertThat(order).hasStatus(OrderStatus.DRAFT)
.hasItemCount(3)
.isNotEligibleForFreeShipping();
} |
|
Что касается Java Records, они трансформировали способ реализации объектов-значений в Java. С их приходом код стал значительно лаконичнее и читабельнее:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // До Java 14
public final class ProductId {
private final String value;
public ProductId(String value) {
this.value = Objects.requireNonNull(value, "Product ID cannot be null");
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProductId productId = (ProductId) o;
return value.equals(productId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
// С Java 14+
public record ProductId(String value) {
public ProductId {
Objects.requireNonNull(value, "Product ID cannot be null");
}
} |
|
Сокращение кода впечатляет! А преймушества не только в краткости — records автоматически генерируют методы equals(), hashCode() и toString(), что исключает ошибки ручной реализации.
Современные возможности Java для реализации DDD
С выходом Java 14 появились Records — лёгкий способ создания иммутабельных классов данных. Это идеальный инструмент для объектов-значений:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null) throw new IllegalArgumentException("Amount cannot be null");
if (currency == null) throw new IllegalArgumentException("Currency cannot be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(double multiplier) {
return new Money(
this.amount.multiply(BigDecimal.valueOf(multiplier))
.setScale(2, RoundingMode.HALF_UP),
this.currency
);
}
public boolean isGreaterThan(Money other) {
assertSameCurrency(other);
return this.amount.compareTo(other.amount) > 0;
}
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException("Cannot compare different currencies");
}
}
} |
|
Records автоматически генерирует конструктор, геттеры, equals(), hashCode() и toString(), что избавляет от боилерплейта и снижает вероятность ошибок. При этом, как видно из примера, мы всё ещё можем добавлять бизнес-методы и дополнитльную логику валидации.
Другая полезная особенность Java 16+ — паттерн матчинга для instanceof, который упрощает работу с полиморфными доменными объектами:
| Java | 1
2
3
4
5
6
7
8
9
| public Money calculateDiscount(DiscountStrategy strategy, Order order) {
if (strategy instanceof PercentageDiscount percentDiscount) {
return order.getTotalAmount().multiply(percentDiscount.getRate());
} else if (strategy instanceof FixedAmountDiscount fixedDiscount) {
return fixedDiscount.getAmount().min(order.getTotalAmount());
} else {
return Money.ZERO;
}
} |
|
Text switches из Java 17 также могут пригодиться при работе с перечислениями и строковыми идентификаторами в доменной модели.
Эти нововведения в Java делают реализацию DDD-принципов более элегантной и снижают объём служебного кода, позволяя сосредоточится на бизнес-логике.
Паттерны Specification и репозитории с Spring Data
Паттерн Specification (Спецификация) — один из недооцененных, но чрезвычайно полезных паттернов в арсенале DDD. Он позволяет инкапсулировать правила и критерии для выборки и фильтрации обьектов. Особенно элегантно этот паттерн сочетается со Spring Data JPA. Начнём с базового интерфейса:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public interface Specification<T> {
boolean isSatisfiedBy(T candidate);
default Specification<T> and(Specification<T> other) {
return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
}
default Specification<T> or(Specification<T> other) {
return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
}
default Specification<T> not() {
return candidate -> !this.isSatisfiedBy(candidate);
}
} |
|
Теперь мы можем создавать конкретые спецификации, которые инкапсулируют бизнес-правила:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class HighValueOrderSpecification implements Specification<Order> {
private final Money threshold;
public HighValueOrderSpecification(Money threshold) {
this.threshold = threshold;
}
@Override
public boolean isSatisfiedBy(Order order) {
return order.getTotalAmount().isGreaterThan(threshold);
}
}
public class UrgentOrderSpecification implements Specification<Order> {
@Override
public boolean isSatisfiedBy(Order order) {
return order.getDeliveryDate() != null &&
order.getDeliveryDate().isBefore(LocalDate.now().plusDays(2));
}
} |
|
Красота паттерна в том, что спецификации можно комбинировать, создавая сложные условия из простых компонентов:
| Java | 1
2
3
4
| Specification<Order> priorityOrders = new HighValueOrderSpecification(new Money(BigDecimal.valueOf(1000), Currency.getInstance("USD")))
.or(new UrgentOrderSpecification());
List<Order> importantOrders = orderRepository.findAllSatisfying(priorityOrders); |
|
Интеграция с Spring Data JPA позволяет преобразовывать эти спецификации в SQL-запросы:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderSpecificationConverter specConverter;
@Override
public List<Order> findAllSatisfying(Specification<Order> spec) {
// Конвертация доменной спецификации в JPA-спецификацию
org.springframework.data.jpa.domain.Specification<OrderEntity> jpaSpec =
specConverter.toJpaSpecification(spec);
return jpaRepository.findAll(jpaSpec)
.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
} |
|
Spring Data JPA имеет свой интерфейс Specification, который можно использовать для построения динамических запросов, но важно разделять доменные спецификации (которые работают с объектами домена) и JPA-спецификации (которые генерируют SQL).
Спецификации в DDD играют важную роль не только для проверки домена, но и для организации сложных запросов. Часто приходится сталкиваться с необходимостью объединить фильтрацию по нескольким критериям, особенно в пользовательских интерфейсах с динамическими фильтрами. Вот элегантная реализация конвертера, который трансформирует доменные спецификации в JPA-спецификации:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| @Component
public class OrderSpecificationConverter {
public org.springframework.data.jpa.domain.Specification<OrderEntity> toJpaSpecification(
Specification<Order> domainSpec) {
if (domainSpec instanceof HighValueOrderSpecification highValueSpec) {
Money threshold = highValueSpec.getThreshold();
return (root, query, cb) -> cb.greaterThan(
root.get("totalAmount"), threshold.amount().doubleValue());
}
if (domainSpec instanceof OrdersByDateRangeSpecification dateRangeSpec) {
return (root, query, cb) -> cb.between(
root.get("orderDate"),
dateRangeSpec.getStartDate(),
dateRangeSpec.getEndDate());
}
// Обработка композитных спецификаций
if (domainSpec instanceof AndSpecification<Order> andSpec) {
return toJpaSpecification(andSpec.getLeft())
.and(toJpaSpecification(andSpec.getRight()));
}
throw new UnsupportedSpecificationException("Unsupported specification type");
}
} |
|
В реальных проектах мне приходилось работать с фильтрами, насчитывающими до 15-20 параметров. Спецификации помогли избежать "взрыва" комбинаций методов репозитория.
Пагинация и сортировка также прекрасно сочетаются со спецификациями:
| Java | 1
2
3
4
5
6
7
8
| @Override
public Page<Order> findAllSatisfying(Specification<Order> spec, Pageable pageable) {
org.springframework.data.jpa.domain.Specification<OrderEntity> jpaSpec =
specConverter.toJpaSpecification(spec);
return jpaRepository.findAll(jpaSpec, pageable)
.map(mapper::toDomain);
} |
|
Для обработки сложных поисковых запросов я использую фабрику спецификаций, которая строит их из DTO с критериями поиска:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Component
public class OrderSpecificationFactory {
public Specification<Order> createFromCriteria(OrderSearchCriteria criteria) {
Specification<Order> spec = Specification.all();
if (criteria.getMinAmount() != null) {
spec = spec.and(new HighValueOrderSpecification(criteria.getMinAmount()));
}
if (criteria.getDateFrom() != null && criteria.getDateTo() != null) {
spec = spec.and(new OrdersByDateRangeSpecification(
criteria.getDateFrom(), criteria.getDateTo()));
}
return spec;
}
} |
|
Такой подход позволяет легко масштабировать фильтрацию, добавляя новые критерии без изменения существующего кода, что соответсвует принципу открытости/закрытости.
Реализация репозиториев с помощью Spring Data
Spring Data значительно упрощает реализацию репозиториев, но важно не допустить проникновения инфраструктурных деталей в домен. Хорошей практикой является создание двух уровней репозиториев:
1. Доменные репозитории — интерфейсы, определённые в доменном слое:
| Java | 1
2
3
4
5
6
7
| // Domain layer
public interface OrderRepository {
Order findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findAllSatisfying(Specification<Order> spec);
void save(Order order);
} |
|
2. Инфраструктурные JPA-репозитории — реализации на основе Spring Data:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // Infrastructure layer
@Repository
public interface OrderJpaRepository extends JpaRepository<OrderEntity, OrderIdJpaEmbeddable> {
List<OrderEntity> findByCustomerId(String customerId);
}
@Service
public class SpringDataOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderMapper mapper;
@Override
public Order findById(OrderId id) {
return jpaRepository.findById(mapper.toJpaId(id))
.map(mapper::toDomain)
.orElseThrow(() -> new OrderNotFoundException(id));
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepository.findByCustomerId(customerId.value())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
// Другие методы...
} |
|
Такое разделение обеспечивает инверсию зависимостей — домен не зависит от инфраструктуры, что способствует тестируемости и устойчивости архитектуры.
Использование Java Annotations для маркирования концепций DDD в коде
Когда команда разрастается, а кодовая база увеличивается в геометрической прогрессии, четкое обозначение архитектурных границ и DDD-концепций становится критически важным. Одним из элегантных решений, которое я активно использую в своих проектах, является создание собственных аннотаций Java для маркирования различных элементов DDD-модели.
Начнем с определения базового набора аннотаций:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {
String boundedContext() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ValueObject {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AggregateRoot {
String boundedContext() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DomainService {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DomainEvent {
String value() default "";
} |
|
Теперь можно явно маркировать классы и методы, чтобы сделать их роль в DDD очевидной:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @AggregateRoot(boundedContext = "ordering")
public class Order {
// ...
@DomainEvent("order.confirmed")
public void confirm() {
// ...
}
}
@ValueObject
public record Money(BigDecimal amount, Currency currency) {
// ...
}
@Entity(boundedContext = "catalog")
public class Product {
// ...
} |
|
Эти аннотации не просто документируют код — они могут быть использованы для автоматической валидации архитектурных ограничений, построения документации или визуализации модели домена. Например, можно написать инструмент, который генерирует UML-диаграмму вашей DDD-модели на основе аннотаций.
Я создал простой процессор аннотаций, который проверяет соответствие кода DDD-принципам:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class DddRulesValidator {
public void validate(Class<?> clazz) {
if (clazz.isAnnotationPresent(ValueObject.class)) {
validateValueObject(clazz);
}
if (clazz.isAnnotationPresent(Entity.class) ||
clazz.isAnnotationPresent(AggregateRoot.class)) {
validateEntity(clazz);
}
}
private void validateValueObject(Class<?> clazz) {
// Проверка: все поля final? Есть equals/hashCode?
}
private void validateEntity(Class<?> clazz) {
// Есть ли идентификатор? Правильно ли реализованы equals/hashCode?
}
} |
|
Такой подход помогает новым членам команды быстрее понять структуру проекта и предотвращает случайное нарушение архитектурных принципов. В одном из моих проектов эти аннотации в сочетании с правилами ArchUnit сократили время онбординга новых разработчиков примерно на 30%.
Продвинутые техники реализации DDD
После освоения базовых принципов DDD самое время погрузиться в продвинутые техники, которые позволяют решать по-настоящему сложные проблемы проектирования. Именно здесь проявляется настоящая красота и мощь предметно-ориентированного дизайна.
Обработка сложных бизнес-правил
В реальных проектах бизнес-правила редко бывают простыми. Они состоят из условий, исключений из условий и исключений из исключений. Как справиться с этой сложностью, не превращая код в спагетти-монстра?
Одним из элегантных решений является паттерн "Правила" (Rules) в сочетании с паттерном "Цепочка ответственности" (Chain of Responsibility):
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public interface BusinessRule<T> {
boolean evaluate(T target);
String getMessage();
}
public class OrderMustHaveItemsRule implements BusinessRule<Order> {
@Override
public boolean evaluate(Order order) {
return !order.getItems().isEmpty();
}
@Override
public String getMessage() {
return "Заказ должен содержать хотя бы один товар";
}
}
public class RuleEngine<T> {
private final List<BusinessRule<T>> rules = new ArrayList<>();
public RuleEngine<T> addRule(BusinessRule<T> rule) {
rules.add(rule);
return this;
}
public Optional<BusinessRule<T>> findFirstViolation(T target) {
return rules.stream()
.filter(rule -> !rule.evaluate(target))
.findFirst();
}
public boolean validate(T target) {
return findFirstViolation(target).isEmpty();
}
} |
|
Такой подход позволяет декларативно описывать правила и комбинировать их в различные последовательности. Он особенно полезен, когда бизнес-правила меняются часто, что характерно для сложных доменов.
Еще одна интерестная техника — использование DSL (предметно-ориентированного языка) для выражения особо сложных бизнес-правил:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class DiscountRuleBuilder {
private Predicate<Customer> customerCondition = c -> true;
private Predicate<Order> orderCondition = o -> true;
private Function<Order, Money> discountCalculator = o -> Money.ZERO;
public DiscountRuleBuilder forCustomersWho(Predicate<Customer> condition) {
this.customerCondition = condition;
return this;
}
public DiscountRuleBuilder forOrdersThat(Predicate<Order> condition) {
this.orderCondition = condition;
return this;
}
public DiscountRuleBuilder applyDiscount(Function<Order, Money> calculator) {
this.discountCalculator = calculator;
return this;
}
public DiscountRule build() {
return new DiscountRule(customerCondition, orderCondition, discountCalculator);
}
} |
|
Такой билдер позволяет создавать четкие, декларативные правила:
| Java | 1
2
3
4
5
| DiscountRule loyalCustomerDiscount = new DiscountRuleBuilder()
.forCustomersWho(c -> c.getLoyaltyLevel() == LoyaltyLevel.GOLD)
.forOrdersThat(o -> o.getTotalAmount().isGreaterThan(Money.of(500, USD)))
.applyDiscount(o -> o.getTotalAmount().multiply(0.1))
.build(); |
|
Проектирование для параллельного изменения
Один из главных вызовов в сложных доменах — необходимость поддерживать несколько версий бизнес-логики одновременно. В моей практике был случай, когда новая регуляторная логика должна была быть внедрена постепенно, при этом старая логика продолжала работать для существующих клиентов.
Элегантным решением стало использование паттерна "Стратегия" в сочетании с фабрикой:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public interface TaxCalculationStrategy {
Money calculateTax(Order order);
}
@Service
public class TaxRuleFactory {
private final Map<TaxRegime, TaxCalculationStrategy> strategies;
private final CustomerRepository customerRepository;
public TaxCalculationStrategy getStrategyFor(CustomerId customerId) {
Customer customer = customerRepository.findById(customerId);
return strategies.get(customer.getTaxRegime());
}
} |
|
Интеграция CQRS и защита от анти-паттернов
Разрабатывая сложные системы на основе DDD, рано или поздно сталкиваешься с фундаментальной проблемой: модель, идеальная для изменения данных, редко бывает оптимальной для их чтения. Command Query Responsibility Segregation (CQRS) — элегантное решение этой дилеммы, разделяющее операции чтения (queries) и записи (commands) на отдельные модели. В классическом приложении запрос на получение данных проходит через все те же слои и агрегаты, что и команда на изменение. Для простых случаев это работает, но когда нужно отобразить сложный дашборд с данными из разных агрегатов и даже контекстов — модель начинает трещать по швам. CQRS предлагает радикальное решение: разные модели для разных целей.
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Команда - используем богатую доменную модель
public class PlaceOrderCommand {
private final CustomerId customerId;
private final List<OrderItemDto> items;
private final DeliveryAddressDto address;
}
// Запрос - прямой доступ к оптимизированному представлению
public class OrderSummaryQuery {
private final CustomerId customerId;
}
// Результат запроса - плоская структура без доменной логики
public class OrderSummaryDto {
private String orderId;
private String customerName;
private String status;
private BigDecimal totalAmount;
private List<String> productNames;
} |
|
Команды обрабатываются в рамках доменной модели, с агрегатами и всеми прелестями DDD. Запросы же могут напрямую обращаться к специализированным представлениям или даже отдельной базе данных, оптимизированой для чтения.
В одном из моих проектов внедрение CQRS спасло нас от производительной катастрофы. Сложный отчет по поставкам собирал данные из пяти различных агрегатов, и каждый запрос превращался в настоящий фейерверк из JPA-запросов. После разделения моделей и внедрения выделенных представлений время генерации отчета уменьшилось с 12 секунд до 400 миллисекунд!
Помимо производительности, CQRS дает и другие преймущества:
1. Возможность независимого масштабирования чтения и записи.
2. Изоляция изменений модели чтения от модели записи.
3. Простота создания специализированных представлений для конкретных UI-сценариев.
Однако тут важно не перестараться. CQRS добавляет сложность, и для простых CRUD-приложений эта сложность не оправдана. Я начинаю с единой модели и перехожу к CQRS только когда вижу конкретные проблемы, которые этот паттерн решает.
Теперь о защите от опасных анти-паттернов. При внедрении DDD легко угодить в несколько ловушек:
1. Анемичная доменная модель — когда сущности превращаются в простые контейнеры данных, а вся логика уходит в сервисы. Это фактически процедурное программирование в ООП-обёртке. Защита: помещайте бизнес-методы в сущности, делая их по-настоящему богатыми обьектами.
2. Единая модель для всей системы — попытка создать "одно кольцо всевластия", универсальную модель, удовлетворяющую всем потребностям. Это ведет к противоречивым концепциям и запутанным абстракциям. Защита: выделяйте ограниченные контексты, где каждый термин имеет четкое значение.
3. Репозитории-монстры — гигантские интерфейсы репозиториев с десятками методов для всевозможных сценариев. Защита: создавайте узко-специализированные репозитории или используйте спецификации.
4. Сервис-аккумуляторы — сервисы, которые стягивают на себя логику из десятков разных сценариев. Защита: выделяйте отдельные классы для каждого сценария использования (например, паттерн Application Service).
5. Игнорирование инвариантов — недостаточная защита бизнес-правил. Защита: инкапсулируйте изменения в агрегатах, используя бизнес методы вместо публичных сеттеров.
Полноценный пример приложения с анализом кода
Давайте рассмотрим полноценное приложение, реализованное с использованием DDD-принципов. Я выбрал в качестве примера упрощенную систему управления заказами интернет-магазина — достаточно понятную предметную область, позволяющую продемонстрировать взаимодействие различных концепций DDD.
Структура приложения
Наше приложение состоит из трёх ограниченных контекстов:
Catalog — управление каталогом товаров,
Sales — обработка заказов и платежей,
Shipping — управление доставкой.
Для каждого контекста определена своя модель предметной области:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| com.example.shop
├── catalog
│ ├── domain
│ │ ├── model
│ │ │ ├── Product.java
│ │ │ ├── ProductId.java
│ │ │ └── ...
│ │ ├── repository
│ │ └── service
│ ├── application
│ └── infrastructure
├── sales
│ ├── domain
│ │ ├── model
│ │ │ ├── Order.java
│ │ │ ├── OrderLine.java
│ │ │ └── ...
│ │ ├── repository
│ │ └── service
│ ├── application
│ └── infrastructure
└── shipping
├── domain
└── ... |
|
Давайте разберём ключевые элементы структуры на примере контекста Sales:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| // Идентификатор заказа - объект-значение
public record OrderId(UUID value) {
public OrderId {
Objects.requireNonNull(value, "Order ID value must not be null");
}
public static OrderId generateNew() {
return new OrderId(UUID.randomUUID());
}
}
// Агрегат Order - корень агрегата
public class Order {
private final OrderId id;
private CustomerId customerId;
private final List<OrderLine> orderLines = new ArrayList<>();
private OrderStatus status;
private DeliveryAddress deliveryAddress;
private Money totalAmount;
protected Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.DRAFT;
this.totalAmount = Money.zero(Currency.USD);
}
// Бизнес-метод для добавления товара
public void addProduct(ProductId productId, int quantity, Money unitPrice) {
validateDraftStatus();
// Поиск существующей позиции
Optional<OrderLine> existingLine = findOrderLineByProductId(productId);
if (existingLine.isPresent()) {
existingLine.get().increaseQuantity(quantity);
} else {
orderLines.add(new OrderLine(OrderLineId.generateNew(), productId, quantity, unitPrice));
}
recalculateTotal();
}
// Метод подтверждения заказа
public void confirm() {
validateDraftStatus();
if (orderLines.isEmpty()) {
throw new EmptyOrderException("Cannot confirm an empty order");
}
if (deliveryAddress == null) {
throw new MissingDeliveryAddressException("Delivery address is required");
}
status = OrderStatus.CONFIRMED;
}
private void validateDraftStatus() {
if (status != OrderStatus.DRAFT) {
throw new OrderModificationException(
"Cannot modify order in " + status + " status");
}
}
private void recalculateTotal() {
totalAmount = orderLines.stream()
.map(OrderLine::getLineTotal)
.reduce(Money.zero(Currency.USD), Money::add);
}
// Геттеры и другие бизнес-методы...
} |
|
Важно обратить внимание на несколько ключевых аспектов:
1. Богатая доменная модель — Order не просто контейнер для данных, а полноценный объект с поведением.
2. Защита инвариантов — бизнес-правила вроде "нельзя подтвердить пустой заказ" строго соблюдаются внутри агрегата.
3. Инкапсуляция — все изменения происходят через бизнес-методы, которые контролируют целостность агрегата.
Репозиторий для доступа к агрегату:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Интерфейс репозитория в домене
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
void save(Order order);
}
// Реализация с Spring Data JPA в инфраструктурном слое
@Repository
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaEntityRepository jpaRepository;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(mapper.toJpaId(id))
.map(mapper::toDomain);
}
// Другие методы...
} |
|
А теперь посмотрим на интеграцию этих компонентов в сервисе приложения:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| @Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final ProductCatalogService productCatalog;
private final PaymentService paymentService;
private final ApplicationEventPublisher eventPublisher;
// Конструктор для инъекции зависимостей...
public OrderId createOrder(CreateOrderCommand command) {
// Создание заказа через фабрику
Order order = orderFactory.createOrder(command.getCustomerId());
// Добавление товаров из комманды
for (OrderItemDto item : command.getItems()) {
// Получение актуальной цены из каталога
ProductInfo product = productCatalog.getProductInfo(item.getProductId());
order.addProduct(item.getProductId(), item.getQuantity(), product.getPrice());
}
// Установка адреса доставки
order.setDeliveryAddress(command.getDeliveryAddress());
// Сохранение и возврат ID
orderRepository.save(order);
// Публикация события
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getCustomerId()));
return order.getId();
}
} |
|
Этот пример демонстрирует идеальный баланс между изоляцией домена и его взаимодействием с внешним миром. OrderApplicationService оркестрирует бизнес-процессы, используя богатую доменную модель для выполнения основной логики.
В нашем приложении для каждого ограниченного контекста мы применили слоистую архитектуру, где каждый слой имеет чёткую зону ответственности. Слои в нашем примере расположены от самого внутреннего (ядра) к внешнему:
1. Доменный слой (Domain) — сердце приложения, содержит бизнес-модель и логику.
2. Прикладной слой (Application) — оркестрирует взаимодействие между доменными объектами и внешним миром.
3. Инфраструктурный слой (Infrastructure) — обеспечивает техническую реализацию репозиториев, интеграций и т.д.
4. Интерфейсный слой (UI/API) — взаимодействие с пользователем или другими системами.
Обратите внимание, как организован процесс обработки заказа через REST API:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService orderService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
// Преобразование API-модели в команду домена
CreateOrderCommand command = requestMapper.toCommand(request);
// Выполнение команды через сервис приложения
OrderId orderId = orderService.createOrder(command);
// Создание ответа для клиента
OrderResponse response = new OrderResponse(orderId.value().toString());
return ResponseEntity.ok(response);
}
} |
|
Что здесь интересно с точки зрения архитектуры:
1. Контроллер предельно тонкий — он только преобразует HTTP-запросы в команды и делегирует их сервису приложения.
2. DTO-объекты (CreateOrderRequest и OrderResponse) полностью изолированы от доменной модели. Они представляют контракт API и могут эволюционировать независимо от домена.
3. OrderApplicationService служит "фасадом" для доменных операций, скрывая детали реализации от клиентского кода.
Такая строгая сегрегация обязанностей может показатся избыточной для простых приложений, но с ростом сложности системы она доказывает свою ценность.
Давайте также взглянем на реализацию интеграции между контекстами. В нашем примере каталог товаров и продажи — это разные ограниченные контексты. Для их взаимодействия мы используем анти-коррупционный слой:
| Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Service
public class CatalogProductService implements ProductCatalogService {
private final ProductApiClient productClient;
@Override
public ProductInfo getProductInfo(ProductId productId) {
// Вызов внешнего API
ProductDto productDto = productClient.getProduct(productId.value().toString());
// Трансформация в модель, понятную контексту продаж
return new ProductInfo(
new ProductId(UUID.fromString(productDto.getId())),
new Money(productDto.getPrice(), Currency.getInstance(productDto.getCurrency()))
);
}
} |
|
Здесь ProductCatalogService — это порт в терминологии Hexagonal Architecture, а CatalogProductService — адаптер, предотвращающий "заражение" модели продаж деталями модели каталога.
Метрики качества DDD-решений: как оценить успешность реализации
После внедрения DDD в проект закономерно возникает вопрос — а насколько успешно мы это сделали? Объективная оценка качества архитектурных решений частенько вызывает затруднения даже у опытных разработчиков. В своей практике я выработал несколько ключевых метрик, которые помагают понять, движемся ли мы в правильном направлении.
Первый индикатор — приемстость изменений. Если добавление новой бизнес-функциональности требует точечных изменений в ограниченных участках кода, не вызывая каскадных модификаций по всему проекту — вы на верном пути. Я однажды наблюдал, как команда внедрила новую схему скидок всего за два дня, изменив лишь несколько классов в соответствующем ограниченном контексте.
Второй показатель — понятность для бизнеса. Может ли эксперт предметной области, глянув на имена классов и методов, понять их назначение без дополнительных объяснений? Если код "говорит" на языке бизнеса, вы успешно применили концепцию единого языка.
Не менее важен и уровень связаности агрегатов. Чрезмерные зависимости между агрегатами свидетельствуют о неправильно определёных границах. Подсчитайте количество связей между агрегатами — их должно быть минимальное количество.
Наконец, обратите внимание на тестируемость домена. Хорошо спроектированная доменная модель легко покрывается модульными тестами, которые понятны и бизнес-аналитикам.
Паттерн Domain Model (Модель области определения) Есть у кого нибудь нормальное описание,и рабочие примеры данного Паттерна?)) Срочно Паттерн Domain Model (Модель области определения) КОд С++ Очень срочно нужен любой пример кода данного Паттерна.Прошу помогите.:( Ошибка отправки сообщения: javax.mail.SendFailedException: 553 sorry, that domain isn't in my list of allowed rcpthosts Делаю webmail и пишет такую ошибку
Ошибка отправки сообщения: javax.mail.SendFailedException:... Protobuf-Converter: Преобразует Domain Object в Google Protobuf Message Вот разработали Protobuf-Converter который преобразует Domain Object в Google Protobuf Message.
... Как реализовать Domain-сущность? Доброго времени суток, помогите, пожалуйста, разобраться в следующем вопросе.
Вопрос такой: как... Cross Domain Куки в Safari Добрый день. Подскажите пожалуйста.
Есть скрипт который встраивается на страницу сайта. Есть... Apache2 Tomcat domain name и неадекватное поведение HttpSession Доброго времени суток!
Выложил наконец свой первый вебапп в сеть. Сервер на Ubuntu 16.04, Apache2... ViewModel. Обертки поверх Domain Model для передачи в пользовательский интерфейс. Best practice Вопрос знатокам проектирования 3-layer и n-layer приложений. Встал вопрос о передачи не самой... Domain - объект Здравствуйте.
Есть следующая задача:
Используя объектно-ориентированный анализ, реализовать... Новый принцип Group By Domain (GBD) Всем привет!
Пытаюсь формализовать принципы чистого (понятного) кода. Хочу обсудить собственную... Имеется ли в Eclipse "Design view" или что-нибудь вроде для графического редактирования GUI? NetBeans умеет это сразу. наверняка это как-то можно и в Eclipse. вообще после первого взгляда на... Соблюдаю ли я принципы SOLID и используя ли я Design Pattern как можно проверить соблюдаю ли я принципы SOLID и используя ли я Design Pattern?
|