Правила связанности¶
Правила связанности измеряют, насколько тесно ваши классы зависят друг от друга. Когда классы тесно связаны, изменение одного класса может сломать множество других. Слабо связанный код легче тестировать (можно изолировать класс), легче изменять (меньше побочных эффектов) и легче переиспользовать.
Представьте связанность как провода, соединяющие коробки. Чем больше проводов между двумя коробками, тем труднее передвинуть одну, не потревожив другую.
CBO -- Связанность между объектами¶
Rule ID: coupling.cbo
Что измеряет¶
CBO считает общее количество других классов, с которыми связан данный класс. "Связь" означает либо:
- Этот класс использует другой класс (исходящая зависимость, называемая "эфферентная связанность" или Ce), либо
- Другой класс использует этот класс (входящая зависимость, называемая "афферентная связанность" или Ca)
CBO = |Ca ∪ Ce| (количество уникальных классов в объединении обоих множеств).
Например, если UserService использует UserRepository, Logger, Validator и Mailer (Ce = 4), а его используют UserController и AdminController (Ca = 2), то CBO = 6 (без пересечений).
Как читать значение:
| CBO | Интерпретация |
|---|---|
| 0--7 | Нормальная связанность |
| 8--14 | Умеренная |
| 15--20 | Высокая -- стоит уменьшить зависимости |
| 20+ | Очень высокая связанность |
Пороговые значения¶
Уровень класса (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | > 14 | Warning |
| Error | > 20 | Error |
Уровень пространства имён (включён по умолчанию, требует минимум 3 класса в пространстве имён):
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | > 14 | Warning |
| Error | > 20 | Error |
Пример¶
class ReportGenerator
{
public function __construct(
private UserRepository $userRepo, // +1
private OrderRepository $orderRepo, // +1
private ProductRepository $productRepo, // +1
private Logger $logger, // +1
private CacheInterface $cache, // +1
private EventDispatcher $dispatcher, // +1
private TemplateEngine $templates, // +1
private PdfGenerator $pdf, // +1
private CsvExporter $csv, // +1
private EmailService $email, // +1
private TranslatorInterface $translator, // +1
private ConfigProvider $config, // +1
private MetricsCollector $metrics, // +1
private SecurityChecker $security, // +1
private AuditLogger $audit, // +1
) {}
// Ce = 15, и если ничего не зависит от этого класса, CBO = 15 -> WARNING
}
Как исправить¶
- Разделите класс. Класс с 15+ зависимостями делает слишком много. Выделите группы связанных зависимостей в отдельные сервисы (например,
ReportDataProvider,ReportExporter). - Используйте интерфейсы вместо конкретных классов. Это не снижает CBO напрямую, но делает связанность слабее и гибче.
- Применяйте внедрение зависимостей. Избегайте создания зависимостей внутри класса через
new. Внедряйте их через конструктор, чтобы их можно было заменить. - Рассмотрите паттерн Facade. Оберните группы связанных сервисов за единым интерфейсом.
Особенности реализации¶
Qualimetrix реализует двунаправленную связанность в соответствии с Chidamber & Kemerer (1994): CBO = |Ca ∪ Ce|, подсчитывая количество уникальных классов, которые появляются во входящих или исходящих зависимостях. Если класс A одновременно использует и используется классом B, B считается один раз (семантика объединения множеств), а не два.
- Расширенные типы связанности: Qualimetrix обнаруживает 14 типов связанности, выходя за рамки исходного определения C&K "методы или переменные экземпляра". Учитываются: создание экземпляров, вызовы статических методов, подсказки типов (параметры, типы возврата, свойства), блоки
catch, проверкиinstanceof, константы классов, атрибуты,extends/implementsи использование трейтов (use). - Union- и intersection-типы: Каждый тип в union (
A|B) или intersection (A&B) подсказке типа считается отдельной связью. - Само-ссылки исключены: Обращения к
self,staticиparentвнутри того же класса не считаются связанностью. - Встроенные PHP-классы исключены: Зависимости от классов из дистрибутива PHP (php-src) исключаются из CBO, Ca и Ce — включая базовые классы (
Exception,DateTime,Closure), SPL (ArrayIterator,SplFileInfo) и встроенные расширения (PDO,DOMDocument,Random\Randomizer,CurlHandleи др.). Связанность со стабильными PHP-типами не увеличивает архитектурный риск. Классы из PECL-расширений (например,Redis,Memcached,MongoDB\Driver\Manager) не исключаются и учитываются в CBO как обычные зависимости. Структурные зависимости (extends) всегда сохраняются для расчёта DIT.
Настройка¶
# qmx.yaml
rules:
coupling.cbo:
exclude_namespaces:
- App\Core\ValueObject
scope: application # 'all' (по умолчанию) или 'application' (использует CBO_APP)
class:
warning: 18
error: 25
namespace:
enabled: true
min_class_count: 5
Сокращённая запись с threshold:
bin/qmx check src/ --rule-opt="coupling.cbo:class.warning=18"
bin/qmx check src/ --rule-opt="coupling.cbo:class.error=25"
bin/qmx check src/ --rule-opt="coupling.cbo:namespace.min_class_count=5"
bin/qmx check src/ --rule-opt="coupling.cbo:namespace.enabled=false"
bin/qmx check src/ --rule-opt="coupling.cbo:scope=application"
Разделение фреймворк/приложение¶
По умолчанию CBO считает все зависимости одинаково: импорт 50 типов PhpParser\Node\* весит столько же, сколько зависимость от 50 сервисов приложения. Но фреймворковая связанность — структурная (нельзя убрать без смены фреймворка), а прикладная — архитектурная (должна минимизироваться).
Qualimetrix предоставляет две дополнительные метрики:
| Метрика | Формула | Назначение |
|---|---|---|
cbo_app |
Ca_app + Ce_app (зависимости фреймворка исключены) | Только прикладная связанность |
ce_framework |
Количество исходящих зависимостей на фреймворк | Информационная |
Настройка:
Сопоставление с учётом границ: Psr совпадает с Psr\Log\LoggerInterface, но НЕ с PsrExtended\Custom.
Использование с правилом CBO:
Установите scope: application, чтобы правило CBO проверяло cbo_app вместо cbo:
rules:
coupling.cbo:
scope: application
class:
warning: 10 # более строгие пороги для прикладной связанности
error: 15
Если framework-namespaces не указаны, cbo_app равен cbo (без эффекта).
Нестабильность¶
Rule ID: coupling.instability
Что измеряет¶
Нестабильность измеряет направление зависимостей класса или пространства имён. Она отвечает на вопрос: "Этот класс в основном зависит от других, или другие в основном зависят от него?"
Формула:
Где:
- Ca (Афферентная связанность) = сколько других классов зависят от ЭТОГО класса (входящие)
- Ce (Эфферентная связанность) = от скольких классов зависит ЭТОТ класс (исходящие)
Результат -- число от 0.0 до 1.0:
- I = 0.0 (максимально стабильный) -- много классов зависят от этого, но он ни от чего не зависит. Как фундамент: его нельзя подвинуть, не сломав всё, что сверху. Стабильные классы должны быть абстрактными (интерфейсы, абстрактные классы), чтобы от них было безопасно зависеть.
- I = 1.0 (максимально нестабильный) -- класс зависит от многих, но никто не зависит от него. Как лист на дереве: легко изменить, не затронув ничего. Конкретные реализации обычно должны быть нестабильными.
Как читать значение:
| I (Instability) | Интерпретация |
|---|---|
| 0.0 | Максимально стабильный |
| 0.0--0.3 | Стабильный -- много зависимых классов |
| 0.3--0.7 | Сбалансированный |
| 0.7--1.0 | Нестабильный -- легко менять |
| 1.0 | Максимально нестабильный |
Почему высокая нестабильность -- это плохо?
Нестабильность близкая к 1.0 означает, что у класса много исходящих зависимостей и никто от него не зависит. Хотя это звучит как "свобода для изменений," это также означает, что класс очень чувствителен к изменениям в его зависимостях. Если любая из зависимостей изменится, этот класс может сломаться.
Пороговые значения¶
Уровень класса (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 0.8 | Warning |
| Error | >= 0.95 | Error |
Уровень пространства имён (включён по умолчанию, требует минимум 3 класса):
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 0.8 | Warning |
| Error | >= 0.95 | Error |
Пример¶
// Сильно нестабильный класс (I = 1.0): зависит от 5 вещей, никто не зависит от него
class DailyReportJob
{
public function __construct(
private UserRepository $users,
private ReportGenerator $generator,
private EmailService $email,
private Logger $logger,
private Clock $clock,
) {}
// Ca = 0 (ничто не зависит от этого)
// Ce = 5 (зависит от 5 классов)
// I = 5 / (0 + 5) = 1.0 -> ERROR
}
Как исправить¶
- Сокращайте исходящие зависимости. Меньше
useи параметров конструктора означает меньше Ce. - Вводите абстракции. Если класс -- сервис, который могут использовать другие, выделите интерфейс. Это увеличивает Ca (другие классы будут зависеть от интерфейса) и снижает I.
- Принимайте нестабильность, когда она оправдана. Точки входа -- консольные команды, контроллеры, cron-задачи -- естественно нестабильны (высокий Ce, низкий Ca). Рассмотрите отключение правила для них или повышение порога.
Отклонение от оригинальной спецификации
Robert C. Martin (1994) изначально определял Instability только на уровне пакетов (пространств имён). Qualimetrix расширяет её на уровень классов для более детального анализа. Instability на уровне пространств имён — каноническая метрика по спецификации Martin; на уровне классов — расширение Qualimetrix.
Настройка¶
# qmx.yaml
rules:
coupling.instability:
exclude_namespaces:
- App\Core\ValueObject
class:
max_warning: 0.9
max_error: 1.0
min_afferent: 1 # по умолчанию: 1
namespace:
min_class_count: 5
min_afferent: 1 # по умолчанию: 1
min_afferent -- минимальное афферентное сцепление (Ca), необходимое для проверки класса или пространства имён. По умолчанию: 1 (символы с Ca = 0 пропускаются). Установите 0, чтобы проверять все символы, или 2, чтобы также пропускать символы с единственным зависимым. Символы с малым числом зависимых имеют высокую нестабильность по определению, что архитектурно ожидаемо для конкретных классов-реализаций.
Сокращённая запись с threshold:
bin/qmx check src/ --rule-opt="coupling.instability:class.max_warning=0.9"
bin/qmx check src/ --rule-opt="coupling.instability:class.max_error=1.0"
bin/qmx check src/ --rule-opt="coupling.instability:class.min_afferent=0"
bin/qmx check src/ --rule-opt="coupling.instability:namespace.min_class_count=5"
bin/qmx check src/ --rule-opt="coupling.instability:namespace.min_afferent=2"
Расстояние от главной последовательности¶
Rule ID: coupling.distance
Что измеряет¶
Это правило проверяет баланс между абстрактностью и стабильностью пространства имён (группы классов). Оно основано на идее:
- Стабильные пакеты должны быть абстрактными. Если от вашего пакета зависят многие классы, он должен состоять из интерфейсов и абстрактных классов. Тогда при необходимости изменить поведение вы добавляете новую реализацию, а не модифицируете то, от чего зависят другие.
- Нестабильные пакеты должны быть конкретными. Если ваш пакет зависит от многих, но ничто не зависит от него, он должен содержать конкретные реализации. Нет смысла делать его абстрактным, если никто не будет реализовывать эти абстракции.
Формула:
Где:
- A (Абстрактность) = доля абстрактных классов и интерфейсов от общего числа классов в пространстве имён (0.0 = все конкретные, 1.0 = все абстрактные)
- I (Нестабильность) = метрика нестабильности, описанная выше
Результат -- число от 0.0 до 1.0:
- D = 0.0 -- пространство имён находится на "главной последовательности," что означает сбалансированное сочетание абстрактности и стабильности.
- D = 1.0 -- пространство имён далеко от идеального баланса.
Есть две проблемные зоны:
- Зона боли (внизу слева): стабильный + конкретный. От пакета зависят многие классы, но весь код конкретный. Любое изменение расходится волной. Решение: добавить интерфейсы.
- Зона бесполезности (вверху справа): нестабильный + абстрактный. Пакет полон абстракций, которые никто не реализует. Мёртвый груз.
Как читать значение:
| Distance | Интерпретация |
|---|---|
| 0.0--0.1 | На главной последовательности |
| 0.1--0.3 | Приемлемый баланс |
| 0.3+ | Дисбаланс -- зона боли или бесполезности |
Пороговые значения¶
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 0.3 | Warning |
| Error | >= 0.5 | Error |
Анализируются только пространства имён с минимум 3 классами (настраивается через minClassCount).
Пример¶
Рассмотрим пространство имён App\Payment с 10 классами:
- 0 интерфейсов или абстрактных классов (A = 0.0)
- Много входящих зависимостей из других модулей (I = 0.2, довольно стабильный)
- D = |0.0 + 0.2 - 1| = 0.8 -- далеко от главной последовательности (Error)
Это пространство имён в Зоне боли: оно стабильно (трудно изменить, не сломав других), но не имеет абстракций (каждое изменение требует модификации конкретного кода).
Как исправить¶
- Для пакетов в Зоне боли: Добавьте интерфейсы. Выделите контракты, от которых могут зависеть другие модули, оставив реализации как детали.
- Для пакетов в Зоне бесполезности: Удалите неиспользуемые абстракции или сделайте их конкретными. Абстрактные классы, которые никто не расширяет -- это накладные расходы.
- Стремитесь к главной последовательности: Стабильные пакеты должны быть абстрактными; нестабильные -- конкретными.
Настройка¶
# qmx.yaml
rules:
coupling.distance:
max_distance_warning: 0.4
max_distance_error: 0.6
min_class_count: 5
include_namespaces:
- App\Domain
- App\Infrastructure
exclude_namespaces:
- App\Tests
Сокращённая запись с threshold:
bin/qmx check src/ --rule-opt="coupling.distance:max_distance_warning=0.4"
bin/qmx check src/ --rule-opt="coupling.distance:max_distance_error=0.6"
bin/qmx check src/ --rule-opt="coupling.distance:min_class_count=5"
По умолчанию пространства имён проекта автоматически определяются из composer.json (autoload.psr-4).
ClassRank¶
Rule ID: coupling.class-rank
Что измеряет¶
ClassRank использует алгоритм PageRank на графе зависимостей для определения наиболее "важных" классов в кодовой базе. Принцип работы: если A зависит от B, это означает, что A "голосует" за B. Классы с большим количеством зависящих от них (или с важными зависимыми) получают более высокий ранг.
Высокий ClassRank означает, что класс является критическим узлом -- изменения в нём имеют широкое влияние на всю систему.
Как читать значение:
| ClassRank | Интерпретация |
|---|---|
| Ниже 0.01 | Периферийный класс |
| 0.01--0.02 | Умеренная важность |
| 0.02--0.05 | Важный узел -- изменения имеют широкий эффект |
| 0.05+ | Критическая точка связанности |
Пороговые значения¶
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 0.02 | Warning |
| Error | >= 0.05 | Error |
Пример¶
// Класс, от которого зависят многие другие (высокий ClassRank)
class EventDispatcher
{
public function dispatch(Event $event): void { /* ... */ }
}
// UserService зависит от EventDispatcher -> "голосует" за него
class UserService
{
public function __construct(private EventDispatcher $dispatcher) {}
}
// OrderService тоже зависит от EventDispatcher -> ещё один "голос"
class OrderService
{
public function __construct(private EventDispatcher $dispatcher) {}
}
// PaymentService тоже -> и ещё один
class PaymentService
{
public function __construct(private EventDispatcher $dispatcher) {}
}
// EventDispatcher получает высокий ClassRank, т.к. от него зависят
// многие важные классы. Если его ClassRank >= 0.02 -> WARNING
Как исправить¶
- Извлеките интерфейс. Выделите
EventDispatcherInterface, чтобы зависимости шли на абстракцию, а не на конкретный класс. Это упрощает замену реализации и снижает влияние изменений. - Примените инверсию зависимостей (DIP). Убедитесь, что модули высокого уровня зависят от абстракций, а не от деталей реализации.
- Разделите ответственности god-класса. Если класс имеет высокий ClassRank из-за того, что совмещает множество обязанностей, разбейте его на несколько более узкоспециализированных классов.
Особенности реализации¶
- Алгоритм PageRank с коэффициентом демпфирования 0.85 и максимумом 100 итераций. Сходимость определяется порогом epsilon = 1e-6.
- Ранги суммируются до 1.0 -- каждый ранг представляет долю "важности" класса в общем графе.
- Vendor-классы исключены -- анализируются только классы проекта (присутствующие в репозитории метрик).
- Dangling-узлы (классы без исходящих зависимостей) распределяют свой вес равномерно между всеми узлами.
- Масштабирование sqrt для размера проекта: Поскольку ранги суммируются до 1.0, индивидуальные значения ClassRank естественно уменьшаются с ростом количества классов (эффект разбавления). Для сохранения осмысленности порогов при разных размерах проекта Qualimetrix применяет масштабирующий коэффициент
sqrt(classCount / 100): пороги остаются неизменными для проекта из 100 классов, ослабляются для более крупных проектов и ужесточаются для меньших.
Настройка¶
Сокращённая запись с threshold: