Опирается на правила:
R-FAC-1…R-FAC-2иR-FAC-X1из DDD Tactical Style Guide → раздел 7. Factory.
Важно знать
- Дефолт — конструктор агрегата. Factory вводится только когда конструктор не справляется.
- Triggers для Factory: валидация требует другого агрегата (нельзя создать
Orderдля деактивированногоCustomer), сборка из нескольких частей, выбор подкласса по политике.- Factory возвращает уже валидный агрегат — со всеми инвариантами + с зарегистрированными начальными событиями (
OrderCreated).- Factory не загружает агрегаты из репозитория. Если нужен другой агрегат — он приходит параметром, загрузка — в
UseCaseHandler.- Factory как stateless-объект в
core/, либо как статический метод на самом агрегате (Order.createFor(customer, items)) — оба варианта годятся.R-FAC-X1Factory ради Factory — антипаттерн. Еслиnew Order(id, customerId, items)справляется — лишний слой не нужен.- Factory ≠ Builder. Builder для удобства конструирования (fluent API); Factory — для бизнес-правил создания, без которых агрегат не может быть валиден.
Factory — паттерн, который вводится узко и поздно. На старте сервиса 90% агрегатов создаются обычным конструктором: new Order(id, customerId, items). Factory нужен, когда «создание» — это самостоятельная бизнес-операция, у которой свои правила и зависимости. Если правил нет — Factory лишний слой. Раскрытие раздела 7 гайда.
Когда вводим
R-FAC-1: один из триггеров должен сработать.
Триггер 1 — валидация требует другого агрегата.
public final class OrderFactory {
public Order createFor(Customer customer, List<OrderItem> items) {
if (!customer.isActive()) {
throw new CustomerNotActiveException(customer.id());
}
if (customer.creditLimit().isLessThan(totalOf(items))) {
throw new CreditLimitExceededException(customer.id(), customer.creditLimit());
}
Order order = new Order(OrderId.generate(), customer.id(), items);
return order;
}
}
Конструктор Order не может проверить customer.isActive() и customer.creditLimit() — у него нет ссылки на Customer (R-AGG-5). Factory принимает Customer параметром, проверяет правило, передаёт в Order только CustomerId.
Триггер 2 — выбор подкласса/варианта по политике.
public final class PaymentFactory {
public Payment createFor(Order order, PaymentMethod method) {
return switch (method.type()) {
case CARD -> new CardPayment(PaymentId.generate(), order.id(), method.cardToken(), order.total());
case SBP -> new SbpPayment(PaymentId.generate(), order.id(), method.phone(), order.total());
case CASH -> new CashPayment(PaymentId.generate(), order.id(), order.total());
};
}
}
Полиморфные агрегаты с общим супертипом — редкий случай в нашем стиле, но если возникает (например, разные типы платежей), Factory скрывает выбор подкласса.
Триггер 3 — сборка из нескольких частей.
public final class InvoiceFactory {
public Invoice createFromOrders(Customer customer, List<Order> orders, BillingPeriod period) {
List<InvoiceLine> lines = orders.stream()
.filter(o -> o.status() == OrderStatus.DELIVERED)
.map(o -> new InvoiceLine(o.id(), o.total(), o.deliveredAt()))
.toList();
if (lines.isEmpty()) {
throw new NoOrdersToInvoiceException(customer.id(), period);
}
return new Invoice(InvoiceId.generate(), customer.id(), period, lines);
}
}
Invoice собирается из нескольких Order-ов с фильтрацией и трансформацией. Если бы это пытался делать конструктор Invoice, ему пришлось бы принимать List<Order> — а это нарушение R-AGG-5 (ссылки между агрегатами по ID).
Возвращает валидный агрегат + начальные события
R-FAC-2: на выходе из Factory — агрегат, который готов к save. Все инварианты проверены, все начальные события зарегистрированы.
public final class OrderFactory {
public Order createFor(Customer customer, List<OrderItem> items) {
if (!customer.isActive()) {
throw new CustomerNotActiveException(customer.id());
}
OrderId id = OrderId.generate();
Order order = new Order(id, customer.id(), items);
// конструктор сам регистрирует OrderCreated:
// registerEvent(new OrderCreated(id, customer.id(), order.total()));
return order;
}
}
OrderCreated регистрируется внутри конструктора агрегата (см. R-EVT-X3 — события только в корне). Factory не регистрирует события сам — он создаёт агрегат, который сам себя «представляет миру» через своё начальное событие.
Сценарий вызова:
@Component
@RequiredArgsConstructor
class CreateOrderHandler implements UseCaseHandler<CreateOrder, OrderId> {
private final CustomerRepository customerRepository;
private final OrderRepository orderRepository;
private final OrderFactory orderFactory;
@Override
@Transactional
public OrderId handle(CreateOrder command) {
Customer customer = customerRepository.findById(command.customerId(), SelectMode.NO_LOCK)
.orElseThrow(() -> new CustomerNotFoundException(command.customerId()));
Order order = orderFactory.createFor(customer, command.items());
orderRepository.save(order); // публикует OrderCreated через outbox
return order.id();
}
}
Handler не «создаёт» order вручную, не «регистрирует событие», не «выставляет статус». Factory отдаёт готовый агрегат, save доставляет событие. Чистый поток.
Как Factory не делает
R-FAC-X1: Factory ради Factory — антипаттерн.
// ПЛОХО — Factory без бизнес-значения, просто обёртка над new
public final class OrderFactory {
public Order create(OrderId id, CustomerId customerId, List<OrderItem> items) {
return new Order(id, customerId, items);
}
}
Это бесполезный слой: метод дублирует конструктор, никакой логики не добавляет, тестировать его невозможно (нет правил), переиспользовать нечего.
Когда автор всё-таки склонен к Factory без правил:
- «Чтобы скрыть
new».new— это нормальная Java, скрывать его без причины не нужно. - «Чтобы единообразие — у нас везде Factory». Cargo-cult. Единообразие хорошо там, где есть смысл; здесь его нет.
- «Чтобы можно было мокать». Если в тесте нужен другой
Order, тест строит его прямымnewили builder-ом, не через mock factory.
Дефолт — конструктор. Factory появляется на основании конкретного триггера из R-FAC-1.
Factory в core/, не в адаптере
R-FAC-1 подразумевает: Factory — это часть домена. Живёт в core/<bc>/domain/factory/, не зависит от Spring/jOOQ/HTTP. В нашем стиле он stateless, тестируется unit-тестом, инжектится в UseCaseHandler напрямую как @Component-бин (через @RequiredArgsConstructor):
// core/order/domain/factory/OrderFactory.java
@Component
public final class OrderFactory {
public Order createFor(Customer customer, List<OrderItem> items) { /* ... */ }
}
@Component в core/ — пограничный случай. В строгом Hexagonal (Уровень 3) core/ без Spring; Factory тогда — pure-Java класс, конструируется руками в bootstrap/ или объявляется через @Bean в @Configuration-классе. На Уровне 2 — @Component в core/ допускается (см. Hexagonal: когда переходить).
Альтернатива — статический метод на самом агрегате:
public final class Order extends AggregateRoot<OrderId> {
public static Order createFor(Customer customer, List<OrderItem> items) {
if (!customer.isActive()) {
throw new CustomerNotActiveException(customer.id());
}
return new Order(OrderId.generate(), customer.id(), items);
}
}
Это короче для простых случаев. Когда правила усложняются и появляются зависимости (например, PromoCodeService для расчёта скидки на момент создания) — выносим в отдельный класс OrderFactory.
Factory ≠ Builder
Builder — паттерн конструирования (fluent API, опциональные параметры, читаемая ленточная сборка). Factory — паттерн бизнес-правил создания. Они решают разные задачи.
// Builder — про удобство
Order order = Order.builder()
.id(OrderId.generate())
.customerId(customerId)
.item("p-1", 2, money(100))
.item("p-2", 1, money(50))
.build();
// Factory — про правила
Order order = orderFactory.createFor(customer, items);
Builder уместен в тестах (TestObjectGenerator) и иногда внутри domain-кода — когда у агрегата много опциональных полей и читаемость от builder-а выигрывает. Builder не заменяет Factory: правило «нельзя создать Order для деактивированного Customer» нельзя положить в Order.builder().build() без зависимости от Customer.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Factory ради Factory (просто обёртка над new) | R-FAC-X1 | new Order(...) напрямую |
| Factory загружает агрегаты из репозитория | R-FAC-1 | Загрузка в UseCaseHandler, Factory принимает агрегаты параметрами |
| Factory не регистрирует начальные события | R-FAC-2 | События регистрируются в конструкторе агрегата (см. R-EVT-X3) |
| Builder вместо Factory, когда есть правила | — | Builder для удобства, Factory для бизнес-правил создания |
Куда дальше
- DDD Tactical → раздел 7. Factory — нормативные формулировки
R-FAC-*. - Aggregate Root —
registerEventдля начального события в конструкторе. - Domain Event —
OrderCreatedи публикация после save. - Domain Service — соседний паттерн «логика про два агрегата», часто путают с Factory.
- Hexagonal: когда переходить — про
@Componentв core/ на разных уровнях зрелости.