Правила связанности¶
Правила связанности измеряют, насколько тесно ваши классы зависят друг от друга. Когда классы тесно связаны, изменение одного класса может сломать множество других. Слабо связанный код легче тестировать (можно изолировать класс), легче изменять (меньше побочных эффектов) и легче переиспользовать.
Тот же анализ связанности классифицирует зависимости от настроенных framework-пространств имён для метрик coupling.cbo-app и coupling.ce-framework. Более поздний источник конфигурации заменяет предыдущий список пространств имён; явный пустой список отключает эту классификацию для запуска.
Представьте связанность как провода, соединяющие коробки. Чем больше проводов между двумя коробками, тем труднее передвинуть одну, не потревожив другую.
CBO -- Связанность между объектами¶
Rule ID: coupling.cbo
Судимые метрики: coupling.cbo, coupling.cbo-app, coupling.cbo-own
Что измеряет¶
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+ | Очень высокая связанность |
На уровне пространства имён CBO считает пространства имён, а не классы. CBO пространства имён -- это число различных пространств имён, в которых лежит класс на другой стороне зависимости, пересекающей границу этого пространства имён, в любом направлении. Граница -- всё поддерево пространства имён, та же область, по которой считаются его coupling.ca и coupling.ce, поэтому родительское пространство имён связано с тем, что лежит вне его, и никогда -- со своими вложенными пространствами имён. coupling.ca и coupling.ce на пространстве имён по-прежнему считают классы, поэтому CBO пространства имён обычно меньше каждой из них, и сообщение находки пишет его как CBO: N namespaces.
Другая сторона называется пространством имён, объявившим класс, поэтому число зависит от того, насколько мелко разбит окружающий код: разделение соседнего каталога на вложенные пространства имён поднимает CBO этого пространства имён при неизменных Ca и Ce. Родительское пространство имён у корня проекта связано со многими такими пространствами имён и обычно имеет самый высокий CBO. Пороги уровня пространства имён ниже повторяют числа уровня класса; для этой величины отдельно они не калибровались.
Пространство имён судится по собственным классам. Правило читает coupling.cbo-own -- тот же счёт, взятый по классам, объявленным непосредственно в пространстве имён, где вложенное пространство имён -- такое же пространство имён, как любое другое. У пространства имён без вложенных область одна, и оба ключа там равны. У родителя они расходятся, и значение по поддереву не судится: оно растёт вместе с поддеревом, а его область зависит от того, какие вложенные пространства имён есть в запуске, так что суженный и полный запуски разошлись бы в том, какой код судится. Граница собственной области от этого не зависит, поэтому то, какие пространства имён судятся, не зависит от путей запуска. Значение зависит, как у любой меры связанности (coupling.ca, coupling.instability, coupling.class-rank): зависимость читается из класса, в котором она начинается, поэтому класс, который запуск не анализировал, — в том числе из вложенного пространства имён, — не учитывается, и суженный запуск (projectScope.state: narrowed в JSON-отчёте) может судить пространство имён по меньшему значению, чем полный. Входящие и исходящие связи в сообщении находки -- тоже из собственной области (coupling.ca-own, coupling.ce-own). Пространство имён, не объявляющее ни одного собственного типа, не публикует coupling.cbo-own и не судится; coupling.cbo по поддереву публикуется на каждом пространстве имён (--format=metrics).
Пороговые значения¶
Уровень класса (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 14 | Warning |
| Error | >= 20 | Error |
Уровень пространства имён (включён по умолчанию, судится по coupling.cbo-own, требует минимум 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 \RuntimeExceptionучитывается в CBO, Ca и Ce не больше, чемimplements \Countableили подсказка типа\DateTimeImmutable. Само реброextendsсохраняется, потому что DIT и NOC читают из него наследование.
Отклонение от оригинальной спецификации
Вселенная графа включает каждый именованный класс, интерфейс, trait и enum проекта, даже если у него нет рёбер. CBO, Ca и Ce дедуплицируют логические endpoints и сохраняют необъявленные внешние цели; поэтому декларации с нулевой степенью получают известные нули вместо отсутствующих метрик. Логическая оценка класса проецируется на каждую точную принадлежащую декларацию, чтобы controls, baseline identities и fingerprints оставались независимыми.
Настройка¶
# qmx.yaml
rules:
coupling.cbo:
suppress_namespaces:
- subtree: 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"
Плоский ярлык threshold (сразу для обоих уровней): голый threshold
на верхнем уровне правила — НЕ вложенный в class:/namespace: — задаёт
warning/error одним и тем же значением сразу на обоих измерениях,
class и namespace. Это отличается от complexity.ccn, где верхний
threshold применяется только к уровню callable: у CBO дефолты class и
namespace уже совпадают (14/20), поэтому нет одного «основного» уровня,
которому стоило бы отдавать предпочтение.
Сокращённая форма раскрывается до слияния слоёв
Верхнеуровневый threshold задаёт границы warning/error и для класса,
и для пространства имён в своём слое конфигурации. Пересекающиеся вложенные
пороги или границы в том же слое приводят к отказу с кодом 3; независимые
настройки вроде class.enabled допустимы. Между слоями поздний слой заменяет
только записанные им значения: threshold: 50 в qmx.yaml вместе с
--rule-opt="coupling.cbo:class.warning=1" и
--rule-opt="coupling.cbo:class.error=1" в CLI задают классу границы 1/1,
оставляя пространству имён 50/50.
Верхнеуровневые warning: / error: — тоже опции этого правила. В отличие
от правил сложности, coupling.cbo читает их на собственном верхнем уровне: они
открывают ту же плоскую форму, что и threshold, и задают оба измерения сразу.
Здесь они работали всегда; документируются сейчас потому, что необъявленный ключ
опции отвергается, а отвергать работающий ключ было бы неверно.
Разделение фреймворк/приложение¶
По умолчанию CBO считает все зависимости одинаково: импорт 50 типов PhpParser\Node\* весит столько же, сколько зависимость от 50 сервисов приложения. Но фреймворковая связанность — структурная (нельзя убрать без смены фреймворка), а прикладная — архитектурная (должна минимизироваться).
Qualimetrix предоставляет две дополнительные метрики:
| Метрика | Формула | Назначение |
|---|---|---|
coupling.cbo-app |
Ca_app + Ce_app (зависимости фреймворка исключены) | Только прикладная связанность |
coupling.ce-framework |
Количество исходящих зависимостей на фреймворк | Информационная |
Настройка:
# qmx.yaml
coupling:
framework-namespaces:
- subtree: Symfony
- subtree: PhpParser
- subtree: Psr
- subtree: Amp
Это явные namespace-селекторы. subtree: Psr совпадает с Psr\Log\LoggerInterface, но не с PsrExtended\Custom; также доступны exact и полнообъектный regex.
Использование с правилом CBO:
Установите scope: application, чтобы правило CBO проверяло coupling.cbo-app вместо coupling.cbo:
rules:
coupling.cbo:
scope: application
class:
warning: 10 # более строгие пороги для прикладной связанности
error: 15
Если framework-namespaces не указаны, coupling.cbo-app равен coupling.cbo (без эффекта).
Непривязанный неймспейс фреймворка¶
Rule ID: coupling.unmatched-framework-namespace
Severity: warning (фиксированная)
Что измеряет¶
Селектор из framework-namespaces, с которым не совпало ни одно имя прогона.
Селектор писали, чтобы вынести классы из coupling.cbo-app в
coupling.ce-framework. Промахнувшийся не выносит ничего, и любой вывод о
прикладной связанности делается по более широкому множеству, чем вы просили.
Больше об этом ничто в прогоне не говорит: промах даёт ровно тот же отчёт, что
и прогон вообще без селектора.
The framework namespace "Symfony\Bundle" matched no class the run analysed or
depends on. Nothing was moved out of the application scope for it, so
"coupling.cbo-app" still counts every class it was written to exclude and
"coupling.ce-framework" counts none of them.
Обычные причины: вендорный неймспейс переименовали; написали ведущий обратный
слэш (\Symfony не совпадает никогда — пишите неймспейс так, как он выглядит
в use); зависимость убрали при рефакторинге, а префикс остался в qmx.yaml.
Одна находка на каждый непривязанный селектор, на уровне проекта. Селектор сверяется с обоими концами каждого ребра зависимостей — с целями, где живут внешние классы фреймворка, и с источниками, — потому что именно это множество классифицируют сами метрики.
Когда молчит¶
-
Нет полной измеренной области деклараций. Пропущенные PHP, авторское снятие PHP, generated-исключения или неизвестный состав кода удерживают утверждение об отсутствии имён; одного
coveredreport state недостаточно. -
framework-namespacesне заданы. Ничего не утверждали — нечему и промахиваться. - Селектор совпал. В том числе если он совпал только с вашими же анализируемыми классами: они всё равно выносятся из прикладной области, а это и есть действие опции.
- Граф зависимостей прогона пуст. Проверьте один файл, который ни от чего
не зависит, — классифицировать было нечего, и верный общий
qmx.yamlне получает находок из-за такого запуска. - Граф зависимостей не построен. Тогда не с чем сверять, и прогон молчит, а не обвиняет разом все префиксы.
Настройка¶
Это обычная находка правила, а не ошибка конфигурации: она подчиняется
fail_on, --disable-rule=coupling.unmatched-framework-namespace,
@qmx-ignore coupling.unmatched-framework-namespace и baseline. Общий
qmx.yaml, законно называющий фреймворк, которого в одном из репозиториев нет,
— тот самый случай, ради которого это не отказ.
Сообщение заканчивается указанием конфигурации, записавшей список селекторов
(Written by configuration file "…" (imported by configuration file "qmx.yaml").).
Список из общей базы, установленной или нет, судится: coupling.framework_namespaces,
записанный в вашем файле, целиком заменяет список базы.
RFC -- Response for a Class¶
Метрики: coupling.rfc, coupling.rfc-own, coupling.rfc-external
RFC измеряет размер множества ответов класса: по определению Чидамбера и Кемерера это объединение его методов и целей, непосредственно вызываемых из этих методов:
M содержит каждый объявленный метод, включая абстрактные, и методы,
эффективно импортированные из трейтов. Алиас трейта добавляет доступное имя;
изменение только видимости не добавляет метод. Методы класса перекрывают
методы трейтов, а конфликты трейтов разрешаются через insteadof. Это относится
к именованным классам, трейтам, интерфейсам и enum. Каждый enum также добавляет
неявный метод cases(); backed enum дополнительно добавляет from() и
tryFrom(). Унаследованные методы не
входят в M; прямой вызов унаследованного метода или метода родителя входит
в множество ответов. Вызовы внутри замыкания метода учитываются. Методы
вложенного анонимного класса и выражения вне методов исключаются.
Непрочитанные трейты не добавляют выдуманных методов в M; неразрешённые
цели вызовов используют описанное ниже сведение к имени метода.
coupling.rfc-own— размерM.coupling.rfc-external— число различных непосредственно вызываемых ответов внеM, включая вызванные унаследованные методы. «External» здесь означает отсутствие в эффективном наборе методов измеряемого объявления, а не нахождение вне анализируемого проекта.coupling.rfc— их сумма. Повторные вызовы и вызовы уже собственного метода её не увеличивают.
Для статически известного класса ответ ключуется разрешённым именем класса
и именем метода без учёта регистра. Алиас, полностью квалифицированное и
относительное имя одной цели считаются один раз. Алиас импортированного трейта
ссылается на исходное тело метода при проверке, принадлежит ли вызываемый ответ
собственному набору. Ответы в эффективных телах импортированных методов трейта
используют контекст this/self/static/parent класса-потребителя.
Именованная цель вне собранного набора классов сохраняет разрешённое имя
класса и метода. Известный получатель, в полностью прочитанной иерархии
которого нет нужного метода, сохраняет идентичность получателя и метода. Унаследованная цель через непрочитанных предков не позволяет
определить объявляющее тело и сводится к имени метода.
Какие вызовы учитываются¶
| Форма | Идентичность ответа |
|---|---|
Собственный метод через $this, self или static |
Уже входит в M |
Унаследованный метод или метод через parent |
Разрешённый объявляющий класс и метод, если их можно прочитать |
| Импортированный метод трейта или алиас | Эффективное доступное имя в M; вызываемое тело разрешается в источник |
Получатель с типом параметра без ..., свойства или promoted-свойства |
Разрешённый класс и метод, если объявленный тип определяет один класс |
Присваивание известного получателя локальной переменной; прямая положительная ветка instanceof |
Сохранённый класс и метод в этой поддерживаемой локальной области |
Именованный статический вызов; new ClassName() |
Разрешённый класс и метод; создание использует __construct |
| Nullsafe-вызов | Та же цель, что у обычного вызова метода |
| Цепочка возвращаемых значений, элемент массива, variadic-параметр или нетипизированный получатель | Только имя метода; повторные вызовы этого имени объединяются |
Статический вызов через переменную ($class::method()) |
Разрешённый класс и метод для известного объекта; иначе только имя метода |
| Глобальная функция | Отдельный ответ функции, независимый от ответов методов |
| First-class callable или динамическое имя метода | Исключается |
Сведение к имени метода применяется также при непрочитанных предках или трейтах и неоднозначном поиске метода. Объявленные типы дают статические факты; RFC не отслеживает диспетчеризацию во время исполнения, не выводит возвращаемые типы и не выполняет общий анализ потока или PHPDoc-типов. Поэтому разные неизвестные получатели с одним именем вызываемого метода могут дать один ответ. Это значения метрики: отдельного правила RFC и настраиваемого порога находки нет.
Отклонение от оригинальной спецификации
- Глобальные функции дают ответы, а хуки свойств PHP 8.4 входят в
M: в оригинальной метрике класса нет соответствующих конструкций PHP. Имена свойств в хуках учитывают регистр. Импортированные хуки с конфликтующими источниками свойства пропускаются; совместимость свойств PHP не проверяется. - Методы и адаптации трейтов следуют правилам композиции PHP, включая
вложенные трейты, алиасы, смену видимости и
insteadof. - First-class callable не является вызовом; динамическое имя метода не разрешается и не добавляет ответ.
- Неизвестный получатель, непрочитанный трейт или предок вне анализируемых путей сводится к имени метода. Результат не обещает полного разрешения диспетчеризации во время исполнения.
Пример¶
class InvoiceService
{
public function issue(Gateway $gateway): void
{
$gateway->send();
$gateway->send();
}
}
// rfc-own: 1 (issue); rfc-external: 1 (Gateway::send); rfc: 2
Чем больше множество ответов, тем больше поведения нужно понять и проверить. Разделяйте обязанности, когда класс обращается к слишком многим несвязанным операциям.
Нестабильность¶
Rule ID: coupling.instability
Судимая метрика: coupling.instability у класса; coupling.instability-own у пространства имён
Что измеряет¶
Нестабильность измеряет направление зависимостей класса или пространства имён. Она отвечает на вопрос: "Этот класс в основном зависит от других, или другие в основном зависят от него?"
Формула:
Где:
- Ca (Афферентная связанность) = сколько других классов зависят от ЭТОГО класса (входящие)
- Ce (Эфферентная связанность) = от скольких классов зависит ЭТОТ класс (исходящие)
Результат -- число от 0.0 до 1.0:
- I = 0.0 (максимально стабильный) -- много классов зависят от этого, но он ни от чего не зависит. Как фундамент: его нельзя подвинуть, не сломав всё, что сверху. Стабильные классы должны быть абстрактными (интерфейсы, абстрактные классы), чтобы от них было безопасно зависеть.
- I = 1.0 (максимально нестабильный) -- класс зависит от многих, но никто не зависит от него. Как лист на дереве: легко изменить, не затронув ничего. Конкретные реализации обычно должны быть нестабильными.
Как читать значение:
| I (Instability) | Интерпретация |
|---|---|
| 0.0 | Максимально стабильный -- или вовсе не связанный: при Ca = Ce = 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 |
Уровень пространства имён (включён по умолчанию, требует минимум min_type_count (по умолчанию 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 расширяет её на уровень классов для более детального анализа; метрика на уровне классов — расширение Qualimetrix.
Пространство имён с вложенными пространствами — уже не пакет в смысле Martin, поэтому Qualimetrix публикует два значения, а судит одно. Правило судит coupling.instability-own, взятую по типам, объявленным прямо в пространстве имён (Ca-own и Ce-own), — той же областью, что судят CBO и distance. Значение по поддереву coupling.instability вбирает все исходящие зависимости вложенных пространств, тогда как к родителю, ничего не объявляющему сам, обращаются немногие, поэтому корень ограниченного контекста по построению близок к 1. Оно остаётся опубликованным как агрегатное свидетельство и питает distance по поддереву, health.coupling и диаграмму Martin в HTML-отчёте, но не судится.
Пространство имён, не объявляющее ни одного проанализированного собственного типа, не публикует coupling.instability-own и никогда не судится. Пространство имён только с голыми енумами ключ публикует, но тоже не судится: судимая популяция считает классы, трейты, интерфейсы и енумы с implements. Пространство имён с типом, но без собственных рёбер (Ca-own = Ce-own = 0), несёт 0 по соглашению выше.
Настройка¶
# qmx.yaml
rules:
coupling.instability:
suppress_namespaces:
- subtree: App\Core\ValueObject
class:
max_warning: 0.9
max_error: 1.0
min_afferent: 1 # по умолчанию: 1
namespace:
min_type_count: 5
min_afferent: 1 # по умолчанию: 1
min_afferent -- минимальное афферентное сцепление (Ca), необходимое для проверки класса или пространства имён; для пространства имён это собственное афферентное сцепление coupling.ca-own. По умолчанию: 1 (символы с Ca = 0 пропускаются). Установите 0, чтобы проверять все символы, или 2, чтобы также пропускать символы с единственным зависимым. Символы с малым числом зависимых имеют высокую нестабильность по определению, что архитектурно ожидаемо для конкретных классов-реализаций.
namespace.min_type_count -- минимальное число типов, объявленных в пространстве имён непосредственно: классы, трейты, интерфейсы и енумы с implements, как у Distance. По умолчанию 3 (равенство допускается); 0 отключает минимум, но пространство имён судится, только если объявляет хотя бы один учитываемый тип при любом минимуме — при том же условии существует coupling.distance-own. Instability и distance — обе метрики пакетов Martin (D = |A + I - 1|) и судят одну популяцию. Прежний min_class_count считал классы всего поддерева и отвергается по имени — в YAML, как min-class-count и minClassCount, и в --rule-opt — с кодом выхода 3; переименуйте ключ и учтите новый подсчёт.
Сокращённая запись с 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_type_count=5"
bin/qmx check src/ --rule-opt="coupling.instability:namespace.min_afferent=2"
Плоский ярлык threshold (сразу для обоих уровней): голый threshold
на верхнем уровне правила — НЕ вложенный в class:/namespace: — задаёт
max_warning/max_error одним и тем же значением сразу на обоих
измерениях, поскольку их дефолты уже совпадают (0.8/0.95).
Сокращённая форма раскрывается до слияния слоёв
Как и у coupling.cbo, верхнеуровневый threshold раскрывается в границы
класса и пространства имён в своём слое. Пересекающиеся сокращённая и явная
записи границ в одном слое приводят к отказу с кодом 3. Во втором слое
вложенная запись заменяет только адресованные значения: например,
--rule-opt="coupling.instability:class.threshold=0.8" поверх плоского
threshold: 0.9 из файла задаёт классу 0.8/0.8, сохраняя пространству имён
0.9/0.9.
Верхнеуровневые max_warning: / max_error: — тоже опции этого правила.
Как и warning/error у coupling.cbo, они открывают плоскую форму на
собственном верхнем уровне правила и задают оба измерения сразу. Здесь они
работали всегда и документируются сейчас по той же причине.
Расстояние от главной последовательности¶
Rule ID: coupling.distance
Судимая метрика: coupling.distance-own
Что измеряет¶
Это правило проверяет баланс между абстрактностью и стабильностью пространства имён (группы классов). Оно основано на идее:
- Стабильные пакеты должны быть абстрактными. Если от вашего пакета зависят многие классы, он должен состоять из интерфейсов и абстрактных классов. Тогда при необходимости изменить поведение вы добавляете новую реализацию, а не модифицируете то, от чего зависят другие.
- Нестабильные пакеты должны быть конкретными. Если ваш пакет зависит от многих, но ничто не зависит от него, он должен содержать конкретные реализации. Нет смысла делать его абстрактным, если никто не будет реализовывать эти абстракции.
Формула:
Где:
- 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+ | Дисбаланс -- зона боли или бесполезности |
Отклонение от оригинальной спецификации
Robert C. Martin (1994) определял Abstractness только через абстрактные и
конкретные классы, поэтому каждый появившийся позже конструкт PHP приходится
отображать на эту модель. Qualimetrix считает голый енум нейтральным и
полностью исключает его из знаменателя, а не относит к конкретике:
перечисление литералов не даёт ни одной точки подстановки -- его нельзя
расширить, у него не бывает подтипов и реализаций. enum X implements Y
считается конкретным и остаётся в знаменателе: это конкретная реализация
объявленного контракта, и без этой оговорки пространство имён с одним
интерфейсом и N реализующими его енумами показывало бы A = 1.0, хотя
реализации лежат рядом. Учитываются только явные implements; интерфейсы
UnitEnum / BackedEnum, которым удовлетворяет любой енум, не учитываются.
Форма формулы не меняется -- это адаптация скоупа под реалии PHP, а не другая
метрика. Пространство имён, где объявлены только голые енумы, имеет пустой
знаменатель и не публикует abstractness или distance. Отсутствующее
значение не является измеренным конкретным A = 0.0.
Две области у пространства имён и проектное число¶
Пространство имён, которое одновременно объявляет собственные классы и содержит вложенные пространства имён, имеет две области связанности, и публикуются обе:
| Ключ | Область |
|---|---|
coupling.ca / coupling.ce / coupling.instability / coupling.abstractness / coupling.distance |
Всё поддерево с корнем в этом пространстве имён; сохраняется как агрегированное свидетельство |
coupling.ca-own / coupling.ce-own / coupling.instability-own / coupling.abstractness-own / coupling.distance-own |
Только типы, объявленные непосредственно здесь; те, что держат вложенные пространства имён, остаются за ними |
coupling.cbo и coupling.cbo-own делятся так же; правило также судит
собственную область: см. CBO.
Instability тоже судится по собственной области (coupling.instability-own):
см. Нестабильность.
При совпадении измеренных популяций и применимости обязательных входов
значения обеих областей одинаковы. Одного отсутствия вложенных областей
недостаточно, чтобы считать входы применимыми. Пространство имён, не
объявляющее ни одного собственного типа, вовсе не несёт собственной
абстрактности и не получает ключей -own для абстрактности, нестабильности и distance (coupling.cbo-own там тоже отсутствует):
насколько пакет удалён от главной последовательности — вопрос, не определённый
там, где пакета нет, и молчание говорит это, а 0.0 не сказал бы.
Публикация и суждение следуют разным популяциям. coupling.instability-own и
coupling.cbo-own публикуются на пространстве имён, объявляющем любой
проанализированный тип; distance и суждение о нестабильности считают только
классы, трейты, интерфейсы и енумы с implements. Пространство имён только с
голыми енумами поэтому несёт оба ключа -own, но не пакет для distance и
нестабильности, даже при min_type_count: 0.
Проектное число сворачивается по собственным областям.
coupling.distance-own.avg — среднее измеренных coupling.distance-own,
взвешенное по числу собственных подходящих типов: классов, трейтов, интерфейсов
и енумов с явным implements. Голые енумы исключены. Пространство имён без
измеренного собственного distance не вносит ни выборку, ни нулевое значение;
.count показывает число измеренных пространств имён; проектный ключ называется
coupling.distance-own.avg, а coupling.distance.avg не существует. Свёртка
роллапов поддерева считала бы объявления родителя один раз за родителя и ещё по
разу за каждого ребёнка. Прежняя свёртка избегала этого двойного счёта, беря
только листовые пространства имён, — и тем самым выбрасывала каждое родительское
пространство имён с собственными классами: в дереве, где промежуточные
пространства имён держат настоящий код, изрядная доля классов не была
представлена в проектном среднем ни одним значением.
Популяция, по которой идёт свёртка, — это же и то, долей чего является строка покрытия здоровья; см. Что именно покрывает оценка.
Пороговые значения¶
| Уровень | Порог | Серьёзность |
|---|---|---|
| Warning | >= 0.3 | Warning |
| Error | >= 0.5 | Error |
Правило судит coupling.distance-own; сообщение использует собственные абстрактность, нестабильность и связанность.
Анализируются пространства имён с минимум 3 собственными типами (настраивается через minTypeCount): классы, трейты, интерфейсы и енумы с явным implements. Голые енумы не учитываются.
Пространство имён без зависимостей в обе стороны (Ca-own = Ce-own = 0) тоже не судится. Его нестабильность равна 0 по соглашению, а не по измерению, поэтому конкретное пространство имён, которое никто не использует и которое ничего не использует, получило бы D = 1.0 и было бы названо зоной боли -- а она требует многих зависимых, которых у него нет. Distance отсутствует и не входит в проектный агрегат или оценку health.
Пространство имён, которому не хватает входа, не публикует расстояние вовсе, а не считает его от 0. Пространство имён, объявляющее только функции, не входит в граф зависимостей классов, поэтому у него нет нестабильности и нет coupling.distance.
Пример¶
Рассмотрим пространство имён 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_type_count: 5
include_namespaces:
- subtree: App\Domain
- subtree: App\Infrastructure
suppress_namespaces:
- subtree: 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_type_count=5"
min_type_count считает классы, трейты, интерфейсы и енумы с явным
implements, объявленные непосредственно в пространстве имён. По умолчанию
3; равенство допускается, а 0 отключает этот минимум. Отсутствующие счётчики
вносят ноль. Правило по-прежнему требует опубликованное собственное расстояние.
Области без связей расстояние не публикуют. Учёт выбранной популяции отличает эти
исключения от здоровых проверенных объектов.
min_class_count, min-class-count и minClassCount для Distance и для
Instability пространства имён отвергаются: та же популяция считается тем же
min_type_count; CBO сохраняет параметр числа классов по собственным классам.
По умолчанию пространства имён проекта автоматически определяются из composer.json (autoload.psr-4).
include_namespaces — список явных namespace-селекторов exact, subtree
или regex; bare scalar отвергается. Универсальная дверь --rule-opt
использует ту же scalar-форму kind:value, поэтому
--rule-opt="coupling.distance:include_namespaces=subtree:App\Domain" выбирает
это поддерево; несколько селекторов пишутся списком в qmx.yaml.
ClassRank¶
Rule ID: coupling.class-rank
Судимая метрика: coupling.class-rank-share
Что измеряет¶
Исходная coupling.class-rank сохраняет вероятностную массу PageRank r.
Правило использует отдельную долю r / (1 / N) = r * N: во сколько раз ранг
вершины превышает равномерный ранг в измеренном графе. Доля 1 равномерная.
N считает различные измеренные логические вершины классовых типов, включая
изолированные, у которых есть логический предмет в хранилище метрик.
Неизмеренные внешние вершины не входят в эту анализируемую популяцию PageRank;
повторные объявления её не увеличивают. Обе метрики
проецируются на точные объявления и публикуют max, average и p95 пространства
имён и проекта. Правило судит точные PHP-классы, включая абстрактные, с
положительным coupling.ca; интерфейсы, трейты и енумы сохраняют свидетельства,
но не судятся. Каждое средство, которое предлагает правило, — выделить интерфейс,
разделить класс, зависеть от абстракции — действует на конкретный класс.
Центральный интерфейс — это и есть результат этих средств, поэтому судить его
значило бы отмечать лечение. Центральный енум — общий закрытый словарь: у него
нет коллабораторов, которые можно разделить, и нечего прятать за интерфейсом.
Трейт копируется в использующие его классы, а не вызывается, поэтому его ранг не
описывает узел во время выполнения. Их ранг и доля остаются в --format=metrics,
а JSON-отчёт считает их в abstentions с гейтом php-class.
Пороговые значения¶
| Уровень | Доля | Серьёзность |
|---|---|---|
| Warning | >= 5 равномерных рангов | Warning |
| Error | >= 10 равномерных рангов | Error |
Равенство достигает порога. Масштабирования через квадратный корень больше нет. Overrides и конфигурация задаются в единицах доли. Сообщение повышает точность от двух до шести знаков, чтобы различить неравные значения; оставшееся совпадение помечается как округление.
Пример¶
// Этот класс используется (прямо или косвенно) большей частью проекта
class DatabaseConnection
{
public function query(string $sql, array $params = []): Result { /* ... */ }
public function beginTransaction(): void { /* ... */ }
public function commit(): void { /* ... */ }
public function rollback(): void { /* ... */ }
}
// UserRepository зависит от DatabaseConnection -> голосует за него
// OrderRepository зависит от DatabaseConnection -> голосует за него
// PaymentService зависит от DatabaseConnection -> голосует за него
// ReportGenerator зависит от DatabaseConnection -> голосует за него
// AuditLogger зависит от DatabaseConnection -> голосует за него
//
// Если эти зависимые классы сами важны (имеют высокий ранг),
// ClassRank DatabaseConnection растёт ещё сильнее.
// ClassRank share = 12 -> ERROR (12 равномерных рангов)
Как исправить¶
- Извлеките интерфейс. Создайте
DatabaseConnectionInterfaceи зависьте от него. Это применяет принцип инверсии зависимостей: модули высокого уровня зависят от абстракций, а не от конкретных классов. - Разделите ответственности god-класса. Если класс делает слишком много, разбейте его. Например, разделите
DatabaseConnectionнаQueryExecutor,TransactionManagerи т. д. - Снизьте транзитивную важность. Если зависимые от этого класса сами являются узлами, рефакторинг их в сторону абстракций снизит и ClassRank этого класса.
Особенности реализации¶
PageRank сохраняет damping 0.85, максимум 100 итераций и epsilon 1e-6.
Dangling-вершины равномерно распределяют ранг. Сумма исходных рангов равна 1,
сумма долей — N до проекции на повторные объявления. Пустой граф не публикует
ни одну метрику, единственная вершина получает исходный ранг 1 и долю 1.
Доля относится к текущей популяции графа; суженный и полный запуск могут
различаться. Приоритизация читает долю, подставляет её измеренную медиану для
находки без ранга и публикует coupling.class-rank-share в JSON top issues. Исходная
вероятность не подставляется вместо отсутствующей доли.
Отклонение от оригинальной спецификации
Агрегаты ClassRank пространства имён и проекта берут физические объявления классов. Каждое объявление повторяет ранг и долю своей логической вершины, поэтому суммы, количества и средние взвешены по объявлениям. Сумма исходного PageRank равна 1 по логическим вершинам до этой проекции, а не по объявлениям.
Рекомендация считает прямых зависимых (Ca). PageRank учитывает и
транзитивные пути зависимостей: один прямой зависимый, который сам является
хабом, может дать высокую долю. Число транзитивных зависимых отчёт не считает.
Distance и Instability расширяют точность и явно ставят display rounded,
как ClassRank, если значение и порог иначе выглядели бы одинаково.
Настройка¶
bin/qmx check src/ --rule-opt="coupling.class-rank:warning=6"
bin/qmx check src/ --rule-opt="coupling.class-rank:error=12"
Перенеси прежние raw/scaled пороги и записи baseline на метрику доли; подстановки исходного ключа и alias параметра нет. См. пороги по умолчанию.
Независимые пары слоёв¶
CBO и instability раскрывают верхнюю пару в class и namespace в написавшем слое. Явные записи в те же раскрытые листья этого слоя конфликтуют; независимые настройки уровня, например enabled, сохраняются. Настрой разные пары уровней явно или помести перекрывающий override в другой слой. Effective-band validation учитывает написанные и default halves. Направления метрик, формулы и numeric defaults не меняются. Написанный enabled:true намеренно снимает нижний disable. См. конфигурацию.
Позиция и форма dependency type¶
Тип ребра называет позицию ссылки: type_hint для параметра/return,
property_type для property и promoted parameter, constant_type для typed
constant. Union/intersection/DNF не создают отдельные relation kinds.
Graph JSON всегда публикует shape как object, сопоставляющий каждой позиции
отсортированные наблюдённые формы (single, nullable, union,
intersection, dnf), например {"property_type": ["nullable"],
"type_hint": ["union"]}. У ребра без type-shape фактов — shape: {}.
Факты объявления (kind, родители/интерфейсы/traits, сайты attributes,
__toString) переносятся отдельно от рёбер. Неявный Stringable не создаёт
синтетических dependency edges и не увеличивает coupling. Логическая PHP
class identity ASCII-folded; точные source declarations остаются отдельными.