Опирается на правила:
R-FAC-1…R-FAC-2иR-FAC-X1из DDD Tactical Style Guide → раздел 7. Factory.
Важно знать
- Дефолт — конструктор агрегата
NewOrder(id, customerID, clock). Factory вводится только при реальном триггере.- Три триггера: (1) валидация требует другого агрегата, (2) выбор варианта по политике, (3) сборка из нескольких источников.
- Factory возвращает
(*Aggregate, error). Если ошибка — агрегат не создаётся.- Factory не загружает из репозитория. Нужные агрегаты приходят параметрами — загрузка в UseCase Handler.
- В Go Factory — обычно пакетная функция
CreateOrder(p CreateOrderParams) (*Order, error)или метод stateless-struct.R-FAC-X1: обёртка надNewOrderбез бизнес-правил — лишний слой, не Factory.
Factory — паттерн создания, вводимый узко. Большинство агрегатов рождаются через конструктор. Factory появляется, когда «создание» само по себе содержит бизнес-правила. Раскрытие раздела 7 гайда на Go-стеке.
Когда вводим
Триггер 1 — валидация требует другого агрегата
Конструктор Order не может проверить customer.IsActive() — у него нет ссылки на Customer (R-AGG-5). Factory принимает Customer параметром:
// core/order/aggregate/order_factory.go
package aggregate
import (
"fmt"
"time"
"github.com/google/uuid"
"example.com/svc/core/customer/aggregate"
"example.com/svc/core/order/vo"
)
type CreateOrderParams struct {
Customer *aggregate.Customer
Clock func() time.Time
NewID func() uuid.UUID
}
func CreateOrder(p CreateOrderParams) (*Order, error) {
if !p.Customer.IsActive() {
return nil, fmt.Errorf("customer %s is not active", p.Customer.ID())
}
id := p.NewID()
customerID := vo.NewCustomerID(p.Customer.ID())
return NewOrder(id, customerID, p.Clock), nil
}
NewOrder остаётся конструктором базового создания и регистрирует OrderCreated. Factory добавляет только правила, зависящие от Customer.
Handler:
func (h *CreateOrderHandler) Handle(ctx context.Context, cmd CreateOrder) (uuid.UUID, error) {
customer, err := h.customers.ByID(ctx, cmd.CustomerID)
if err != nil {
return uuid.Nil, err
}
order, err := aggregate.CreateOrder(aggregate.CreateOrderParams{
Customer: customer,
Clock: h.clock,
NewID: uuid.New,
})
if err != nil {
return uuid.Nil, err
}
if err := h.orders.Save(ctx, order); err != nil {
return uuid.Nil, err
}
return order.ID(), nil
}
Триггер 2 — выбор варианта по политике
// core/payment/aggregate/payment_factory.go
package aggregate
import (
"fmt"
"github.com/google/uuid"
"example.com/svc/core/payment/vo"
)
func CreatePayment(orderID uuid.UUID, method vo.PaymentMethod, amount vo.Money) (Payment, error) {
switch method.Type() {
case vo.PaymentTypeCard:
return newCardPayment(uuid.New(), orderID, method.CardToken(), amount), nil
case vo.PaymentTypeSBP:
return newSBPPayment(uuid.New(), orderID, method.Phone(), amount), nil
default:
return Payment{}, fmt.Errorf("unsupported payment method: %s", method.Type())
}
}
newCardPayment и newSBPPayment — пакетные конструкторы для конкретных вариантов. Factory скрывает выбор.
Триггер 3 — сборка из нескольких источников
// core/invoice/aggregate/invoice_factory.go
package aggregate
import (
"fmt"
"time"
"github.com/google/uuid"
orderagg "example.com/svc/core/order/aggregate"
"example.com/svc/core/invoice/vo"
)
func CreateInvoiceFromOrders(
customerID uuid.UUID,
orders []*orderagg.Order,
period vo.BillingPeriod,
clock func() time.Time,
) (*Invoice, error) {
var lines []InvoiceLine
for _, o := range orders {
if o.Status() != orderagg.OrderStatusDelivered {
continue
}
total, _ := o.Total()
lines = append(lines, NewInvoiceLine(o.ID(), total, o.DeliveredAt()))
}
if len(lines) == 0 {
return nil, fmt.Errorf("no delivered orders for period %s", period)
}
return newInvoice(uuid.New(), customerID, period, lines, clock()), nil
}
Invoice собирается из нескольких Order-ов с фильтрацией. Если бы это делал конструктор Invoice, он бы получал []*Order — нарушение R-AGG-5.
Возвращает валидный агрегат
R-FAC-2: Factory возвращает агрегат, готовый к Save. Все инварианты проверены, начальные события зарегистрированы (в конструкторе агрегата):
func NewOrder(id uuid.UUID, customerID vo.CustomerID, clock func() time.Time) *Order {
o := &Order{
AggregateBase: shared.NewAggregateBase[uuid.UUID](id),
customerID: customerID,
status: OrderStatusNew,
}
o.registerEvent(event.NewOrderCreated(uuid.New(), clock(), id, customerID.Value()))
return o
}
CreateOrder вызывает NewOrder — событие регистрируется там. Factory не регистрирует события сам.
Антипаттерн — Factory без бизнес-правил
R-FAC-X1:
// ПЛОХО — обёртка над конструктором без логики
func CreateOrder(id uuid.UUID, customerID vo.CustomerID, clock func() time.Time) (*Order, error) {
return NewOrder(id, customerID, clock), nil
}
// ХОРОШО — если хватает конструктора, вызываем его напрямую
order := aggregate.NewOrder(uuid.New(), customerID, h.clock)
Когда нет правил, нет триггера — Factory лишний слой.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Factory без бизнес-правил (обёртка над конструктором) | R-FAC-X1 | NewOrder(id, customerID, clock) напрямую |
| Factory загружает агрегат из репозитория | R-FAC-1 | Загрузка в Handler; Factory принимает объекты параметрами |
| Невалидный агрегат на выходе (без проверки инвариантов) | R-FAC-2 | Factory возвращает (*Order, error), ошибка при нарушении |
| Factory регистрирует события напрямую | R-EVT-X3 | События — в конструкторе агрегата через registerEvent |
Куда дальше
- DDD Tactical → раздел 7. Factory — нормативные формулировки
R-FAC-*. - go/aggregate-root.md — конструктор агрегата и
registerEventдля начальных событий. - go/domain-service.md — Domain Service как соседний паттерн «логика на двух агрегатах».
- go/module-structure.md — куда кладётся
aggregate/order_factory.go.