Опирается на правила: R-FAC-1R-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-X1NewOrder(id, customerID, clock) напрямую
Factory загружает агрегат из репозиторияR-FAC-1Загрузка в Handler; Factory принимает объекты параметрами
Невалидный агрегат на выходе (без проверки инвариантов)R-FAC-2Factory возвращает (*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.