Перейти к содержанию

Правила поддерживаемости (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.

Как исправить

  1. Извлеките вспомогательные методы. Разбейте длинный метод на меньшие, именованные части:

    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];
    }
    
  2. Уменьшите ветвление. Замените вложенные цепочки if/else на ранние возвраты, паттерн Strategy или полиморфизм.

  3. Используйте value objects. Замените массивы типизированными объектами, чтобы уменьшить количество "сырых" операций.

Совет

Опция minLoc (по умолчанию: 10) отфильтровывает тривиально маленькие методы. Простые геттеры и сеттеры давали бы экстремальные значения MI, не имеющие практического смысла. Скорректируйте этот параметр, если получаете слишком много ложных срабатываний на коротких методах.

Особенности реализации

Индекс поддерживаемости использует формулу Омана-Хагемайстера:

MI = 171 - 5.2 x ln(V) - 0.23 x CCN - 16.2 x ln(LOC)

Где:

  • 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 Пропускать методы с числом строк меньше этого
# qmx.yaml
rules:
  maintainability.index:
    warning: 40
    error: 20
    exclude_tests: true
    min_loc: 10
bin/qmx check src/ --rule-opt="maintainability.index:warning=35"
bin/qmx check src/ --rule-opt="maintainability.index:min_loc=15"