← Назад к вопросам

Как разделять агрегат, чтобы его части оставались небольшими?

3.0 Senior🔥 152 комментариев
#Архитектура и паттерны

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблема больших агрегатов и стратегии декомпозиции

При проектировании систем на основе предметно-ориентированного проектирования (DDD) часто возникает противоречие: агрегат должен быть целостной транзакционной единицей, но при росте бизнес-логики он может стать слишком большим. Монолитный агрегат нарушает принцип единственной ответственности, затрудняет поддержку и снижает производительность из-за блокировок при параллельных операциях.

Критерии для разделения агрегата

Перед декомпозицией нужно убедиться, что это действительно необходимо. Признаки "раздутого" агрегата:

  • Содержит более 5-7 сущностей и value-объектов
  • Имеет десятки методов бизнес-логики
  • Часто блокируется при конкурентных обновлениях
  • Изменяется по разным причинам (нарушает SRP)

Стратегии декомпозиции

1. Выделение ограниченных контекстов

Если части агрегата относятся к разным бизнес-процессам, их стоит разделить на разные ограниченные контексты. Например, агрегат Order может быть разделён:

// Было: один большой агрегат
class Order {
    private OrderId $id;
    private Customer $customer;
    private array $items;
    private Payment $payment;    // Относится к платежам
    private Shipment $shipment;  // Относится к доставке
    private Invoice $invoice;    // Относится к бухгалтерии
}
// Стало: несколько специализированных агрегатов
class Order {
    private OrderId $id;
    private CustomerId $customerId;
    private array $itemIds;
}

class Payment {
    private PaymentId $id;
    private OrderId $orderId;
    private PaymentStatus $status;
}

class Shipment {
    private ShipmentId $id;
    private OrderId $orderId;
    private Address $address;
}

2. Применение правил инвариантов

Ключевой принцип — каждый агрегат должен защищать свои собственные инварианты. Если инварианты можно разделить логически:

// Инварианты заказа: сумма позиций = итоговая сумма
class Order {
    public function addItem(Product $product, int $quantity): void {
        $this->items[] = new OrderItem($product, $quantity);
        $this->recalculateTotal();
        // Инвариант: $this->total == сумма($this->items->getSubtotal())
    }
}

// Инварианты доставки: адрес должен быть валидным для службы доставки
class Shipment {
    public function updateAddress(Address $address): void {
        if (!$this->carrier->isAddressServicable($address)) {
            throw new InvalidAddressException();
        }
        $this->address = $address;
    }
}

3. Использование ссылок вместо вложенных объектов

Замените непосредственное владение объектами на ссылки по идентификатору:

// Вместо хранения всего объекта Customer
class Order {
    private Customer $customer; // Плохо
}

// Используйте идентификатор
class Order {
    private CustomerId $customerId; // Хорошо
    private OrderId $id;
    
    public function getCustomer(): Customer {
        // Получаем через репозиторий при необходимости
        return $this->customerRepository->find($this->customerId);
    }
}

4. Событийно-ориентированная коммуникация

Для поддержания согласованности между разделёнными агрегатами используйте события предметной области:

class Order {
    public function confirm(): void {
        $this->status = OrderStatus::CONFIRMED;
        $this->domainEvents[] = new OrderConfirmed(
            $this->id,
            $this->customerId,
            $this->totalAmount
        );
    }
}

class InventoryManager {
    public function onOrderConfirmed(OrderConfirmed $event): void {
        foreach ($event->getItems() as $item) {
            $this->reserveStock($item->productId, $item->quantity);
        }
    }
}

Практические шаги декомпозиции

  1. Анализ транзакционных границ: какие изменения должны быть атомарными?
  2. Определение консистентности: где нужна сильная согласованность, а где достаточно eventual consistency?
  3. Выделение поддоменов: разбейте по бизнес-процессам (платежи, доставка, инвентаризация)
  4. Проектирование идентификаторов: обеспечьте уникальные ID для связывания агрегатов
  5. Рефакторинг постепенно: не пытайтесь разделить всё сразу

Пример рефакторинга

// До рефакторинга
class ECommerceOrder {
    private $id;
    private $customer;
    private $items = [];
    private $paymentDetails;
    private $shippingAddress;
    private $inventoryReservations;
    
    public function process(): void {
        $this->validatePayment();
        $this->reserveInventory();
        $this->scheduleShipping();
        $this->generateInvoice();
    }
}

// После рефакторинга
class Order {
    private $id;
    private $customerId;
    private $items = [];
    
    public function confirm(): void {
        $this->publish(new OrderConfirmedEvent($this->id, $this->items));
    }
}

// Отдельные агрегаты для других поддоменов
class Payment { /* ... */ }
class Inventory { /* ... */ }
class Shipping { /* ... */ }

Итог: разделение агрегатов требует баланса между целостностью данных и гибкостью архитектуры. Основное правило — агрегат должен быть достаточно маленьким, чтобы управляться как единое целое, но достаточно большим, чтобы защищать свои бизнес-инварианты в рамках одной транзакции.

Как разделять агрегат, чтобы его части оставались небольшими? | PrepBro