Как разделять агрегат, чтобы его части оставались небольшими?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблема больших агрегатов и стратегии декомпозиции
При проектировании систем на основе предметно-ориентированного проектирования (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);
}
}
}
Практические шаги декомпозиции
- Анализ транзакционных границ: какие изменения должны быть атомарными?
- Определение консистентности: где нужна сильная согласованность, а где достаточно eventual consistency?
- Выделение поддоменов: разбейте по бизнес-процессам (платежи, доставка, инвентаризация)
- Проектирование идентификаторов: обеспечьте уникальные ID для связывания агрегатов
- Рефакторинг постепенно: не пытайтесь разделить всё сразу
Пример рефакторинга
// До рефакторинга
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 { /* ... */ }
Итог: разделение агрегатов требует баланса между целостностью данных и гибкостью архитектуры. Основное правило — агрегат должен быть достаточно маленьким, чтобы управляться как единое целое, но достаточно большим, чтобы защищать свои бизнес-инварианты в рамках одной транзакции.