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

Правила поддерживаемости (Maintainability)

Индекс поддерживаемости объединяет несколько метрик в единую оценку, показывающую, насколько легко поддерживать код. Более высокие значения -- лучше, что является противоположностью большинства других правил.

Анализ объединяет входные данные Halstead, вычисление MI и пороговую политику в один конвейер поддерживаемости, поэтому оценка и подтверждающие её метрики остаются согласованными.


Индекс поддерживаемости (Maintainability Index)

Идентификатор правила: maintainability.mi

Судимая метрика: maintainability.mi

Что измеряет

Индекс поддерживаемости (MI) -- это составная оценка, объединяющая три фактора:

  • Объем по Холстеду (Halstead Volume) -- сколько "информации" содержится в коде (на основе количества операторов и операндов)
  • Цикломатическая сложность -- сколько независимых путей выполнения существует в коде
  • Число statements метода -- независимый от форматирования семантический размер метода

Эти три фактора объединяются в одно число. Исходная формула дает значения на шкале 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. Замените массивы типизированными объектами, чтобы уменьшить количество "сырых" операций.

Совет

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

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

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

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

Где:

  • V = Halstead Volume (мера информационного содержания, основанная на операторах и операндах)
  • CCN = Цикломатическая сложность (вариант CCN2+)
  • LOC-вход = size.method-statement-count, независимое от форматирования число исполняемых и control-flow statements

Исходное значение MI (шкала 0--171) нормализуется до шкалы 0--100: max(0, MI x 100 / 171).

Область действия: MI рассчитывается для каждого метода, затем агрегируется на уровень класса/пространства имен/проекта с использованием среднего и минимального значений.

Маппинг в оценку здоровья: Измерение health.maintainability использует штрафную формулу, учитывающую среднее MI (базовое качество), 5-й перцентиль MI (основной дифференциатор для проблемных методов) и минимум MI (экстремальные выбросы). Этот многокомпонентный подход обеспечивает хорошую дифференциацию между проектами — от качественных библиотек (оценка ~95) до сложных фреймворков (оценка ~48). Подробности и настройка — в разделе Оценки здоровья.

Отклонение от оригинальной спецификации: statement-count

Qualimetrix использует для size-члена формулы отдельную метрику size.method-statement-count. Она считает семантические executable/control-flow statements, а не физические строки или старую Halstead-метрику methodLoc. Это делает MI стабильным к форматированию, но меняет существующие значения MI, агрегаты, health scores и baseline.

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

Простой порог вместо раздельных уровней warning/error (threshold нельзя сочетать с warning или error — смешение считается ошибкой конфигурации, и прогон останавливается с кодом 3):

rules:
  maintainability.mi:
    threshold: 20   # warning=20, error=20 → все нарушения ниже 20 становятся ошибками
bin/qmx check src/ --rule-opt="maintainability.mi:threshold=20"