Правила поддерживаемости (Maintainability)¶
Индекс поддерживаемости объединяет несколько метрик в единую оценку, показывающую, насколько легко поддерживать код. Более высокие значения -- лучше, что является противоположностью большинства других правил.
Индекс поддерживаемости (Maintainability Index)¶
Идентификатор правила: maintainability.index
Что измеряет¶
Индекс поддерживаемости (MI) -- это составная оценка, объединяющая три фактора:
- Объем по Холстеду (Halstead Volume) -- сколько "информации" содержится в коде (на основе количества операторов и операндов)
- Цикломатическая сложность -- сколько независимых путей выполнения существует в коде
- Строки кода -- физический размер метода
Эти три фактора объединяются в одно число. Исходная формула дает значения на шкале 0--171, хотя на практике большинство кода попадает в диапазон 0--100.
Как читать оценку:
| Оценка | Значение |
|---|---|
| 85--100+ | Отлично -- легко понять и изменить |
| 65--84 | Хорошо -- разумная поддерживаемость |
| 40--64 | Умеренно -- может выиграть от упрощения |
| 20--39 | Плохо -- трудно поддерживать, рекомендуется рефакторинг |
| Ниже 20 | Критично -- очень трудно поддерживать, рефакторинг необходим |
Инвертированные пороги
В отличие от большинства правил, где высокие значения хуже, здесь низкие значения хуже. Правило срабатывает, когда оценка падает ниже порога.
Пороговые значения¶
| Оценка | Серьезность | Значение |
|---|---|---|
| 40+ | OK | Поддерживаемый код |
| 20--39 | Warning | Поддерживаемость ухудшается |
| Ниже 20 | Error | Код очень трудно поддерживать |
Пример¶
Метод с низкой поддерживаемостью (MI около 15):
public function processOrder(array $items, array $discounts, ?Customer $customer): array
{
$result = [];
$total = 0;
$taxRate = 0.0;
if ($customer !== null) {
if ($customer->isPremium()) {
$taxRate = $customer->getRegion() === 'EU' ? 0.20 : 0.15;
if ($customer->hasLoyaltyCard()) {
$taxRate *= 0.95;
}
} else {
$taxRate = match ($customer->getRegion()) {
'EU' => 0.21,
'US' => 0.08,
'UK' => 0.20,
default => 0.10,
};
}
}
foreach ($items as $item) {
$price = $item['price'] * $item['quantity'];
foreach ($discounts as $discount) {
if ($discount['type'] === 'percentage') {
if (in_array($item['category'], $discount['categories'], true)) {
$price *= (1 - $discount['value'] / 100);
}
} elseif ($discount['type'] === 'fixed') {
if ($price > $discount['min_amount']) {
$price -= $discount['value'];
}
} elseif ($discount['type'] === 'bogo') {
if ($item['quantity'] >= 2) {
$freeItems = intdiv($item['quantity'], 2);
$price -= $freeItems * $item['price'];
}
}
}
$tax = $price * $taxRate;
$result[] = [
'item' => $item['name'],
'subtotal' => $price,
'tax' => $tax,
'total' => $price + $tax,
];
$total += $price + $tax;
}
return ['items' => $result, 'total' => $total];
}
У этого метода высокая сложность, много строк и много операторов/операндов -- все это снижает оценку MI.
Как исправить¶
-
Извлеките вспомогательные методы. Разбейте длинный метод на меньшие, именованные части:
public function processOrder(array $items, array $discounts, ?Customer $customer): array { $taxRate = $this->calculateTaxRate($customer); $result = []; $total = 0; foreach ($items as $item) { $lineItem = $this->processLineItem($item, $discounts, $taxRate); $result[] = $lineItem; $total += $lineItem['total']; } return ['items' => $result, 'total' => $total]; } -
Уменьшите ветвление. Замените вложенные цепочки
if/elseна ранние возвраты, паттерн Strategy или полиморфизм. -
Используйте value objects. Замените массивы типизированными объектами, чтобы уменьшить количество "сырых" операций.
Совет
Опция minLoc (по умолчанию: 10) отфильтровывает тривиально маленькие методы. Простые геттеры и сеттеры давали бы экстремальные значения MI, не имеющие практического смысла. Скорректируйте этот параметр, если получаете слишком много ложных срабатываний на коротких методах.
Особенности реализации¶
Индекс поддерживаемости использует формулу Омана-Хагемайстера:
Где:
- V = Halstead Volume (мера информационного содержания, основанная на операторах и операндах)
- CCN = Цикломатическая сложность (вариант CCN2+)
- LOC = Логические строки кода (LLOC -- количество выражений, а не физических строк)
Исходное значение MI (шкала 0--171) нормализуется до шкалы 0--100: max(0, MI x 100 / 171).
Область действия: MI рассчитывается для каждого метода, затем агрегируется на уровень класса/пространства имен/проекта с использованием среднего и минимального значений.
Маппинг в оценку здоровья: Измерение health.maintainability использует штрафную формулу, учитывающую среднее MI (базовое качество), 5-й перцентиль MI (основной дифференциатор для проблемных методов) и минимум MI (экстремальные выбросы). Этот многокомпонентный подход обеспечивает хорошую дифференциацию между проектами — от качественных библиотек (оценка ~95) до сложных фреймворков (оценка ~48). Подробности и настройка — в разделе Оценки здоровья.
Входные данные LOC
Qualimetrix использует LLOC (логические строки -- количество выражений) для формулы MI, что соответствует оригинальной статье Омана-Хагемайстера. Некоторые инструменты используют физические LOC (включая пустые строки и комментарии) или ELOC (исполняемые строки), что дает другие результаты. LLOC дает наиболее стабильные и осмысленные значения, поскольку не зависит от форматирования или плотности комментариев.
Halstead Volume: семантический подход
Halstead Volume (V в формуле) использует семантическую интерпретацию методологии Halstead (1977). Qualimetrix считает только элементы, несущие семантическую нагрузку (арифметические, логические, операторы сравнения; переменные, литералы, константы) и исключает синтаксические разделители (;, (), {}, ,). Оригинальная работа Halstead считала все токены, но была разработана для языков (Fortran, PL/I) с минимальным синтаксическим шумом. Инструменты, считающие все токены (например, pdepend), выдают значительно более высокие значения Volume/Difficulty/Effort. Это не влияет на относительные сравнения между методами в рамках одного проекта.
Конфигурация¶
| Опция | По умолчанию | Описание |
|---|---|---|
enabled |
true |
Включить или выключить правило |
warning |
40.0 |
Оценка ниже этой вызывает предупреждение |
error |
20.0 |
Оценка ниже этой вызывает ошибку |
excludeTests |
true |
Пропускать тестовые файлы |
minLoc |
10 |
Пропускать методы с числом строк меньше этого |