Исследование архитектуры с Qualimetrix¶
Это руководство описывает практические подходы к анализу архитектуры PHP-проектов с помощью Qualimetrix. Оно основано на реальном анализе проектов: Doctrine ORM, Laravel, Symfony Console, Composer, PHP-Parser, Guzzle, Monolog и Flysystem.
Поэтапное погружение¶
Самый эффективный способ исследования кодовой базы -- поэтапное погружение от общего к частному. Представьте карту: сначала континент, потом страна, потом город.
Шаг 1: Общая картина¶
Сводка показывает оценки здоровья по пяти измерениям (сложность, связность, зацепление, типизация, поддерживаемость), худшие пространства имён и худшие классы. Это подсказывает, куда смотреть в первую очередь.
Tip
Метки уровня здоровья (Excellent / Good / Fair / Poor / Critical) дают быструю оценку серьёзности. Но сравнивайте числовые значения, а не метки -- шесть проектов с меткой "Fair" могут иметь баллы от 45 до 68.
Шаг 2: Погружение в проблемные пространства имён¶
Вид пространства имён показывает оценки здоровья по нему, дочерние пространства и худшие классы внутри. Ищите пространства имён с оценкой ниже 50 (Poor или Critical).
Note
Вы можете увидеть два балла: свёрнутый (включая подпространства) и прямой (только классы непосредственно в данном пространстве). Пространство имён со свёрнутым баллом 79.7% и прямым 39.5% означает, что проблемы именно в прямых классах, а подпространства в порядке.
Шаг 3: Исследование конкретных классов¶
Флаг --detail показывает нарушения на уровне отдельных методов. Обратите внимание на разбивку «Техдолг по правилам», чтобы решить, какой тип рефакторинга приоритизировать.
Шаг 4: Верификация по исходному коду¶
Qualimetrix указывает номера строк для каждого нарушения. Откройте отмеченные методы и прочитайте код. Метод с когнитивной сложностью 107 на строке 342 -- это то, к чему можно перейти сразу.
Анализ зацепления и зависимостей¶
Анализ зацепления (coupling) показывает, насколько тесно связаны ваши классы и пространства имён.
Рекомендуемый порядок действий¶
-
Начните с измерения зацепления:
-
Проверьте циклические зависимости отдельно -- это самые приоритетные проблемы зацепления:
-
Погрузитесь в худшие пространства имён и посмотрите на нестабильность (I), абстрактность (A) и расстояние от главной последовательности (D):
-
Фокусируйтесь на ошибках CBO (порог 20), а не предупреждениях (порог 14). Ошибки CBO указывают на классы, которые, вероятно, нуждаются в декомпозиции.
Интерпретация метрик зацепления¶
CBO (Coupling Between Objects) считает все классы, связанные с данным -- как те, от которых он зависит (эфферентные, Ce), так и те, что зависят от него (афферентные, Ca).
Warning
Высокий CBO у интерфейса не означает плохой дизайн. Интерфейс с CBO=45, где Ca=44 (много зависящих) и Ce=1, -- это успешная точка абстракции. Проблемой является только высокое эфферентное зацепление (Ce) у интерфейсов.
Нестабильность на уровне класса часто является шумом. Листовой конкретный класс с I=1.00 (никто от него не зависит) -- это архитектурно правильно. Фокусируйтесь на нестабильности на уровне пространства имён -- там метрика становится полезной.
ClassRank (PageRank, применённый к графу зависимостей) полезен как инструмент обнаружения, а не строгое правило. Классы с наивысшим рангом -- это архитектурный центр. Сопоставьте с CBO и LCOM, чтобы определить, является ли высокая центральность здоровой (ключевая абстракция) или проблемной (god-объект).
Note
Значения ClassRank зависят от размера проекта. В проекте из 1500 классов даже самый центральный класс может иметь низкий абсолютный ранг (0.03) по сравнению с проектом из 130 классов (0.10). Не сравнивайте ClassRank между проектами разного размера.
Расстояние от главной последовательности требует настройки пространств имён при анализе сторонних библиотек. Без этого вы можете получить ноль результатов:
bin/qmx check vendor/doctrine/orm/src/ \
--rule-opt='coupling.distance:include_namespaces=Doctrine\ORM'
Циклические зависимости¶
Малые циклы (2-3 класса) наиболее практичны для исправления. Цикл вроде HelperSet <-> HelperInterface указывает на конкретную двунаправленную зависимость, которую можно разорвать. Большие циклы (100+ классов) обычно проходят через центральный хаб (например, EntityManager) и требуют архитектурного рефакторинга, а не быстрого исправления.
Анализ сложности¶
Метрики сложности помогают находить методы, которые трудно понять, трудно тестировать или и то, и другое.
Какую метрику использовать¶
Когнитивная сложность -- лучшая метрика для ответа на вопрос «трудно ли понять этот метод?». Она штрафует за глубину вложенности, которая сильно коррелирует с трудностью понимания. Почти нулевой уровень ложных срабатываний.
- Когнитивная > 15: метод стоит внимательно просмотреть
- Когнитивная > 30: метод почти наверняка нуждается в рефакторинге
- Когнитивная > 100: god-метод, который необходимо разделить
NPath Complexity -- лучший сигнал для ответа «трудно ли тестировать этот метод?». Считает количество уникальных путей выполнения.
- NPath > 200: стоит проверить покрытие тестами
- NPath > 1000: почти наверняка недостаточное покрытие тестами
- Нарушения только по NPath (без когнитивной) сигнализируют «трудно тестировать исчерпывающе», а не «трудно читать»
Цикломатическая сложность (CCN) оценивает минимальное количество тестов для полного покрытия ветвей. Следите за паттерном switch/match: если CCN высокий, а когнитивная сложность низкая, метод, вероятно, представляет собой таблицу соответствий или диспетчер типов -- это не настоящая проблема сложности.
Стратегия приоритизации¶
- В первую очередь -- ошибки когнитивной сложности (> 30)
- Для предупреждений (15-30) проверьте, отмечен ли CCN тоже:
- Оба отмечены: метод стоит исследовать
- Только когнитивная: проблема вложенности, стоит посмотреть
- Только CCN: вероятно, switch/match, низкий приоритет
- Нарушения только по NPath: проверяйте покрытие тестами, а не тратьте время на ревью кода
Чтение расхождений между метриками¶
Соотношение метрик рассказывает историю:
| Паттерн | Что это значит |
|---|---|
| Высокие CCN + когнитивная + NPath | Действительно сложный код. Исследуйте немедленно. |
| Высокий CCN + низкая/нет когнитивной | Механическое ветвление (switch/match). Низкий приоритет. |
| Высокий NPath + низкие CCN и когнитивная | Независимые условия. Проблема тестируемости. |
| Высокая когнитивная + низкий CCN | Сложность из-за вложенности. Трудно следить, несмотря на мало ветвей. |
Tip
Выражение match с 20 ветвями даёт CCN=20, но когнитивную=0-1. Это раздувает CCN без соответствующей трудности понимания. Если вы видите CCN > 20 без нарушения когнитивной сложности, это почти наверняка switch/match -- пропустите для целей ревью.
Анализ связности и дизайна классов¶
Метрики связности (cohesion) помогают находить классы, которые пытаются делать слишком многое и должны быть разделены.
Начните с обнаружения god-классов¶
Обнаружение god-классов -- самое полезное правило, связанное со связностью. Для каждого обнаружения:
- Совпадение 4/4 критериев (высокий WMC, высокий LCOM, низкий TCC, большой размер) почти всегда указывает на настоящий god-класс
- Совпадение 3/4 критериев требует ручной проверки -- проверьте, следует ли класс известному паттерну (фабрика, визитёр, форматтер), где низкая связность допустима
- Если TCC >= 0.5, класс измеримо связен и может быть ложно отмечен по критерию LCOM
Использование LCOM4 для решений о разделении¶
LCOM4 считает количество компонент связности в графе «метод-поле». Класс с LCOM4=3 скорее всего можно разделить на 3 целевых класса.
- LCOM4 от 2 до 5: у класса, вероятно, столько же различных ответственностей. Изучите, какие методы делят какие поля, чтобы найти естественные точки разделения
- LCOM4 > 10: обычно указывает на паттерн-класс (фабрика, форматтер, обработчик), где высокий LCOM допустим
Когда игнорировать TCC¶
TCC (Tight Class Cohesion) измеряет, какая доля пар методов обращается к одним и тем же свойствам. Метрика имеет смысл только для классов с 4+ неконструкторными методами и 2+ свойствами. На маленьких value-объектах, AST-узлах, событиях или DTO TCC=0 ожидаемо и не является проблемой.
Note
Статические утилитные классы (вроде Str или Arr) исключаются из измерения TCC (все методы статические, нет пар экземплярных методов для сравнения). Формула здоровья присваивает таким классам нейтральное значение 0.5.
Обнаружение data-классов¶
Правило data-class имеет более высокий уровень ложных срабатываний, чем обнаружение god-классов, особенно на:
- Интерфейсах (100% WOC по определению)
- Классах исключений (простые по дизайну)
- Маленьких сервисных классах с чистым API
Используйте excludeReadonly: true и excludePromotedOnly: true для кодовых баз с PHP 8.2+ DTO. Для классов исключений и интерфейсов подавляйте через @qmx-ignore design.data-class.
Интерпретация code smell¶
Не все обнаруженные запахи кода одинаково срочны. Вот руководство по приоритизации на основе соотношения сигнал/шум.
Реагируйте немедленно (высокий сигнал)¶
eval-- всегда расследуйте. Даже легитимные использования должны быть задокументированыgoto-- редко и обычно поддаётся рефакторингуcount-in-loop-- простое исправление, реальное влияние на производительностьconstructor-overinjection-- сигнализирует о проблемах дизайнаsensitive-parameter-- простое и ценное улучшение безопасности
Настройте под ваш контекст¶
long-parameter-list-- порог 4/6 по умолчанию разумен; для legacy-кода рассмотрите 5/8god-class-- просматривайте нарушения уровня WARNING (3/4 критерия) ежеквартальноboolean-argument-- ожидайте ~50% ложных срабатываний во фреймворковом коде; значительно меньше в прикладном
Подавляйте или отключайте выборочно¶
debug-code-- подавляйте в файлах, которые намеренно предоставляют API для отладкиdata-class-- подавляйте для классов исключений и DTOidentical-subexpression-- исключайте сгенерированные файлы через--excludeили baselineempty-catch-- подавляйте для паттернов цепочки ответственности; всегда добавляйте комментарий с объяснением
Мониторьте, но не блокируйте CI¶
superglobals-- информационно во фреймворковом коде, требует внимания в прикладномerror-suppression-- часто необходимо для файловых операций; рассматривайте индивидуальноexit-- легитимно в CLI-точках входа; проблемно в библиотечном коде
Правила безопасности¶
- Включайте
sensitive-parameterбезусловно -- высокий сигнал, мало шума hardcoded-credentialsтребует ручной проверки -- ожидайте ложные срабатывания в файлах переводов и конфигурации- Правила инъекций (
sql-injection,xss,command-injection) -- вспомогательные. Не полагайтесь на них как на основной инструмент проверки безопасности
Сравнение проектов¶
При сравнении оценок здоровья между проектами учитывайте следующие оговорки.
Чему доверять¶
- Оценки типизации -- самый надёжный дискриминатор. Основаны на простом подсчёте (типизированные объявления / все объявления) без сложной агрегации
- Оценки зацепления хорошо различают проекты и совпадают с интуицией. Маленькие целевые библиотеки набирают высокие баллы; большие взаимосвязанные фреймворки -- низкие
- Общее здоровье -- разумный составной сигнал
К чему относиться скептически¶
- Оценки сложности для маленьких проектов (< 100 классов) могут быть искажены единственным пространством имён с несколькими god-классами. Балл 0 не обязательно означает «критически сложная кодовая база»
- Оценки поддерживаемости имеют минимальный разброс между проектами и лучше подходят для отслеживания динамики внутри одного проекта
- Количество нарушений может быть раздуто доменными ложными срабатываниями (например, 782 нарушения identical-subexpression в коде парсера). Всегда проверяйте разбивку нарушений по правилам
Чек-лист для сравнения¶
- Сравнивайте числовые баллы, а не метки -- метки могут группироваться слишком плотно
- Для сложности проверяйте также среднее CCN на метод наряду с оценкой здоровья
- Если в проекте < 100 классов, относитесь к оценкам сложности и зацепления с дополнительной осторожностью
- Проверьте, не доминирует ли одно правило в количестве нарушений
- Для фреймворкового кода рассмотрите отключение правил типизации для фокуса на структурных проблемах
Автоматизированный анализ через JSON¶
Для CI-конвейеров или архитектурных ревью с помощью ИИ JSON-вывод предоставляет структурированные данные.
Рекомендуемый порядок действий¶
# Шаг 1: Обзорное сканирование
bin/qmx check src/ --format=json --workers=0
# Шаг 2: Глубокое погружение в конкретное пространство имён
bin/qmx check src/ --namespace=App\\Domain --format=json --workers=0
# Шаг 3: Сырые метрики для индивидуального анализа
bin/qmx check src/ --format=metrics --workers=0
Наиболее полезные поля JSON¶
| Поле | Назначение |
|---|---|
health.* |
Быстрая оценка здоровья проекта по 5 измерениям |
worstNamespaces[].healthScores |
Показывает худшее измерение для каждого пространства имён |
worstClasses[].metrics.{cbo, tcc, wmc, mi.avg} |
Диагностика на уровне класса |
violationsMeta.byRule |
Распределение нарушений без необходимости просматривать каждое |
health.overall.directScore |
Различает «это пространство имён плохое» и «его дочерние плохие» |
Tip
Базовый JSON ограничивает нарушения до 50. Используйте --namespace для конкретной области, чтобы получить все нарушения без усечения.
Стабильное ядро vs. изменчивая периферия¶
Используйте нестабильность и абстрактность из метрик JSON для картирования архитектуры:
| Паттерн | Значение |
|---|---|
| Низкая I, высокая A | Стабильное абстрактное ядро -- интерфейсы и контракты. Здорово. |
| Низкая I, низкая A | Зона боли -- стабильный, но конкретный код. Трудно изменять. |
| Высокая I, низкая A | Изменчивая периферия -- конкретные реализации. Ожидаемо для листового кода. |
| Высокая I, высокая A | Зона бесполезности -- абстрактный, но никто от него не зависит. |
Частые ошибки¶
Не начинайте с --detail на всём проекте. Это перегружает. Начните со сводки и погружайтесь постепенно.
Не используйте количество нарушений как метрику приоритета. Класс с 65 предупреждениями "long-parameter-list" менее срочен, чем класс с 5 ошибками сложности.
Не игнорируйте измерения оценки здоровья. Класс с "Excellent" по сложности, но "Critical" по связности требует другого подхода к рефакторингу, чем класс с "Critical" по сложности.
Не сравнивайте значения ClassRank между проектами разного размера. PageRank размывается с ростом графа.
Не полагайтесь только на общую оценку здоровья. Погружайтесь в оценки по измерениям. Проект с "Fair" общей оценкой может скрывать "Critical" по сложности за счёт "Excellent" по типизации.