Правила архитектуры (Architecture)¶
Правила архитектуры выявляют структурные проблемы в кодовой базе, которые могут привести к кошмарам при поддержке. Эти проблемы часто незаметны в повседневной работе, но причиняют значительную боль, когда нужно провести рефакторинг, протестировать или развернуть части приложения независимо.
Циклические зависимости (Circular Dependencies)¶
Идентификатор правила: architecture.circular-dependency
Что измеряет¶
Обнаруживает ситуации, когда классы зависят друг от друга по кругу. Зависимость означает, что один класс использует другой (через внедрение в конструктор, вызовы методов, указания типов и т.д.).
Прямой цикл (размер 2):
OrderService использует PaymentService, а PaymentService использует OrderService. Ни один из них не может существовать без другого.
Транзитивный цикл (размер 3+):
A зависит от B, B зависит от C, а C зависит обратно от A. Петля длиннее, но проблема та же.
Почему это важно¶
Циклические зависимости вызывают реальные проблемы:
- Невозможно тестировать изолированно. Чтобы протестировать класс A, нужен класс B, которому нужен класс C, которому снова нужен A.
- Невозможно развертывать независимо. Если пакеты A, B и C образуют цикл, они должны всегда развертываться вместе.
- Жесткая связанность. Изменения в любом классе цикла могут сломать все остальные классы в цикле.
- Труднее понять. Нет четкого "верха" или "низа" -- нельзя читать код в линейном порядке.
Пороговые значения¶
| Тип цикла | Серьезность | Значение |
|---|---|---|
| Прямой (размер 2) | Error | Два класса напрямую зависят друг от друга |
| Транзитивный (размер 3+) | Warning | Более длинная цепочка классов образует петлю |
Примечание
Прямые циклы (A зависит от B, B зависит от A) по умолчанию отмечаются как Error, потому что они представляют наиболее жесткую связанность. Транзитивные циклы отмечаются как Warning, так как их обычно легче разорвать.
Настройки¶
| Опция | По умолчанию | Описание |
|---|---|---|
enabled |
true |
Включить или выключить правило |
maxCycleSize |
0 |
Максимальный размер цикла для отчета (0 = все размеры) |
directAsError |
true |
Считать прямые циклы (размер 2) ошибками |
Пример конфигурации¶
# qmx.yaml
rules:
architecture.circular-dependency:
maxCycleSize: 5 # игнорировать очень большие циклы
directAsError: true # прямые циклы -- ошибки
Пример¶
// OrderService.php
class OrderService
{
public function __construct(
private PaymentService $paymentService, // зависит от PaymentService
) {}
public function createOrder(Cart $cart): Order
{
$order = new Order($cart);
$this->paymentService->charge($order);
return $order;
}
public function getOrderTotal(int $orderId): float
{
// ...
return $total;
}
}
// PaymentService.php
class PaymentService
{
public function __construct(
private OrderService $orderService, // зависит от OrderService -- ЦИКЛ!
) {}
public function charge(Order $order): void
{
$total = $this->orderService->getOrderTotal($order->id);
// обработка платежа...
}
}
OrderService зависит от PaymentService, а PaymentService зависит от OrderService. Это прямой цикл размера 2.
Как исправить¶
-
Введите интерфейс (инверсия зависимостей). Пусть один класс зависит от абстракции, а не от конкретного класса:
interface OrderTotalProviderInterface { public function getOrderTotal(int $orderId): float; } class OrderService implements OrderTotalProviderInterface { public function __construct( private PaymentService $paymentService, ) {} public function getOrderTotal(int $orderId): float { /* ... */ } } class PaymentService { public function __construct( private OrderTotalProviderInterface $totalProvider, // нет цикла! ) {} } -
Вынесите общую логику в третий класс. Если обоим классам нужны одни и те же данные, извлеките их:
-
Используйте события. Вместо прямых вызовов генерируйте событие, на которое подписывается другой сервис:
Совет
Используйте опцию maxCycleSize, чтобы сначала сосредоточиться на самых критичных циклах. Прямые циклы (размер 2) легче всего исправить и они наиболее вредны. Начните с них, затем переходите к более крупным циклам.