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

Как бороться от появления большого агрегата?

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

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

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

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

Борьба с большими агрегатами в проектировании PHP Backend

Появление больших агрегатов (Large Aggregates) — классическая проблема в проектировании приложений, особенно при использовании DDD (Domain-Driven Design) или любых подходов, где агрегаты являются центральными единицами бизнес-логики. Большой агрегат — это агрегат, который слишком сложен, содержит множество сущностей и валидаций, имеет чрезмерно большой жизненный цикл, что приводит к проблемам с производительностью, сопровождением и соответствием бизнес-правилам. Вот стратегии борьбы с этой проблемой.

Анализ причин и симптомов

Прежде всего, важно определить симптомы большого агрегата:

  • Чрезмерное количество сущностей внутри: Агрегат содержит более 5-7 сущностей, часто включая коллекции с сотнями элементов.
  • Сложные транзакционные границы: Любая операция требует загрузки всего агрегата в память, даже если изменяется одна маленькая часть.
  • Нарушение инкапсуляции: Агрегат начинает экспортировать свои внутренние сущности для внешних манипуляций.
  • Проблемы с конкурентностью: Из-за большого размера агрегат часто становится точкой конфликтов при параллельных изменениях.

Пример "больного" агрегата в PHP:

class OrderAggregate {
    private Order $order;
    private array $lineItems = [];
    private array $payments = [];
    private array $shipments = [];
    private Customer $customer;
    private array $logs = [];
    private array $discounts = [];

    public function calculateTotal(): float {
        // Сложная логика, перебирающая все lineItems, payments, discounts...
        // Вся структура загружается в память для простого расчета.
    }
}

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

1. Рефакторинг границ агрегата через анализ инвариантов

Ключевая задача — пересмотреть инварианты (business invariants), которые агрегат защищает. Если агрегат защищает несколько независимых инвариантов, возможно, он должен быть разбит.

Правило: Агрегат должен защищать один главный инвариант или группу тесно связанных инвариантов.

Пример декомпозиции:

// Вместо одного OrderAggregate:
class Order {
    private OrderId $id;
    private array $lineItems = [];
}

class PaymentHistory {
    private OrderId $orderId;
    private array $payments = [];
}

class ShipmentTracker {
    private OrderId $orderId;
    private array $shipments = [];
}

2. Использование ссылок на другие агрегаты

Если агрегату нужна информация из другого контекста, используйте ссылки по идентификатору, а не включение всей сущности.

class Order {
    private OrderId $id;
    private CustomerId $customerId; // Ссылка, не объект Customer
    private array $lineItems = [];

    public function getCustomerInfo(CustomerRepository $repo): CustomerInfo {
        return $repo->getInfo($this->customerId); // Загрузка по необходимости
    }
}

3. Выделение специализированных агрегатов для коллекций

Если в агрегате есть большая коллекция (например, история событий), выделите ее в отдельный агрегат с собственным корнем.

class Order {
    private OrderId $id;
    // Убрали array $logs
}

class OrderLogAggregate {
    private OrderId $orderId;
    private array $entries = [];
    public function addLog(LogEntry $entry): void {
        // Инварианты только для логов
    }
}

4. Применение принципов CQRS (Command Query Responsibility Segregation)

Для больших агрегатов разделите модели для чтения (Query) и изменения (Command). Агрегат становится тонкой командной моделью, защищающей инварианты, а для чтения используется отдельная, оптимизированная модель (например, проекция в SQL).

// Командная модель (агрегат) - небольшая
class OrderAggregate {
    private OrderId $id;
    private Status $status;
    public function confirm(): void {
        $this->status = Status::CONFIRMED;
    }
}

// Модель для чтения (проекция) - может быть сложной, но без инвариантов
class OrderReadModel {
    public function getOrderDetails(OrderId $id): array {
        // Сложный SQL-запрос, собирающий данные из Order, LineItems, Customer
        return $this->db->fetchAssoc('SELECT * FROM order_projections WHERE id = ?', [$id]);
    }
}

5. Оптимизация загрузки через паттерн Lazy Loading или спецификации

Если полная загрузка необходима, используйте ленивую загрузку только нужных частей или спецификации (Specifications) для фильтрации.

class OrderRepository {
    public function getOrderWithItems(OrderId $id): Order {
        // Загружает только Order и связанные LineItems, но не Payments и Shipments
        $order = $this->find($id);
        $order->loadItems($this->itemRepository->findForOrder($id));
        return $order;
    }
}

Практические шаги внедрения

  • Анализ транзакционных потребностей: Определите, какие операции действительно требуют атомарного изменения нескольких сущностей.
  • Постепенная декомпозиция: Не переписывайте все сразу. Разбивайте агрегат по одному модулю, тестируя инварианты.
  • Усиление защиты границ: После разбиения убедитесь, что новые маленькие агрегаты строго защищают свои границы, не позволяя внешним объектам напрямую изменять внутренние сущности.
  • Мониторинг производительности: Используйте профилирование (например, XHProf в PHP) для отслеживания влияния изменений на время загрузки и память.

Итог: Борьба с большими агрегатами — это прежде всего рефакторинг бизнес-модели, а не просто техническая оптимизация. Необходимо постоянно задавать вопрос: «Что этот агрегат действительно должен защищать?». Правильно размерные агрегаты приводят к более стабильным, производительным и адаптируемым системам, особенно в долгосрочной перспективе развития PHP-приложения.

Как бороться от появления большого агрегата? | PrepBro