среда, 20 июня 2018 г.

Качество кода одним числом

Maintainability Index – величина качества кода

Качество продукта, согласно ISO-9126, состоит из 6 частей (см. Рис.1 "ISO-9126").
Рис.1 "ISO-9126"
Из этих же шести пунктов (см. Рис.1 "ISO-9126") строится и качество кода, которое можно измерить для любого языка программирования. К сожалению, идеального кода не существует и добиться невозможно, как и КПД-100%.
Одна из простейших метрик – количество параметров, которое рекомендуют не более 7: 5 входящих, 2 на выход. Особое место занимают глобальные переменные, наличие которых усложняет код.
Более сложно качество кода вычисляется по формулам, только одна из которых "цикломатическая сложность" пока принята институтом стандартов. Когда научному миру удастся доказать полезность и значимость остальных формул, тестировщики смогут официально ориентироваться на эти лимиты.
Формула расчёта Maintainability Index была выведена достаточно давно Доном Колеманом, Паулем Оманом и Джеком Хегмейстером. Она имеет несколько вариаций для различных языков программирования. Фактически, польза индекса "ремонтопригодности" не только в оценке готового кода, но по этой величине вполне можно спрогнозировать устойчивость кода с учётом будущих исправлений.
Согласно формулам (см. Рис.2 "Две формулы подсчёта количества багов в юните", где TNOpr – общее количество операторов, TNOpd – общее количество операндов, DNOpr – количество уникальных операторов, DNOpd  – количество уникальных операндов) Мариуса Халстеда, если код состоит хотя бы из одного оператора и операнда, то величина багов в этом коде уже не нулевая.
Рис.2 "Две формулы подсчёта количества багов в юните"
Если оценить обе формулы (см. Рис.2 "Две формулы подсчёта количества багов в юните") через их графики, то очевидно, что количество багов в новом и исправленном коде будет нулевым только в "пустой" подпрограмме.
 
Рис.3 "График и формула подсчёта новых багов"
На рисунке 3 "График и формула подсчёта новых багов" верхняя формула для расчёта прогнозируемых багов минимизирована по количеству операторов (1 шт) и операндов (2 шт). Количество багов смотрим по оси Y, количество операторов и операндов (только целые числа) смотрим по оси X. Поскольку рабочей подпрограммы без операторов и операндов не существует, то по графику убеждаемся, что количество багов резко уходит в бесконечность только при нулевых значениях операторов и операндов [0..1], количество багов растёт плавно (y>0 при x>1). 
 
 Рис.4 "График и формула подсчёта недавних багов"
Минимизировав вторую формулу (см. Рис.2 "Две формулы подсчёта количества багов в юните") для расчёта недавних багов и приняв один оператор на пару операторов, получаем по-сути аналогичный график (см. Рис.4 "График и формула подсчёта недавних багов"), когда количество багов равномерно растёт по оси Y и не может быть отрицательным, так как в рабочем коде должен быть хотя бы один оператор и операнд, то есть значения по оси X рассматриваются только целочисленные, начиная с 1.
Итак, мы убедились, что в любом коде всегда есть баги. А об их серьёзности можно судить по величине Maintainability Index (MI).

Из чего же складывается величина качества кода?
Максимальная формула учитывает значения Сложности Халстеда, Цикломатической Сложности, значимые Строки кода и объём Комментариев:
MI = 171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln (LOC) + 50 * sin(sqrt(2.4 * COM)), где HV – сложность Халстеда, CC – цикломатическая сложность, LOC – количество строк кода, COM – объём комментариев в коде.
Укороченная формула не учитывает объём комментариев:
MI = 171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln (LOC)
Компилируемые языки программирования "не отвлекаются" на комментарии, поэтому для них (например, Си) вполне очевидно использование укороченной формулы. А языки-интерпретаторы (например, SQL) корректнее оценивать по полной формуле, так как комментарии внутри цикла в некоторых языках могут замедлять исполнение программы, поскольку на вычленение незначимых строк тоже нужно время.
Пределы индекса принято рассматривать по следующей таблице (см. Рис.5 "Лимиты Maintainability Index"):
- подпрограмма требует немедленного исправления, если MI упал меньше 65;
- подпрограмму желательно исправить, если MI больше или равен 65, но меньше 85;
- подпрограмма не нуждается в исправлении, если MI равен или больше 85.
Исходя из формулы абсолютное максимальное значение Maintainability Index равно 221 (171-0-0-0+50), но оно не достижимо ни при каком содержимом сорсника.
Рис.5 "Лимиты Maintainability Index"
Об истории лимитов MI можно почитать в статье.

Итак, чем больше индекс, тем лучше код. Рассмотрим способы повышения индекса важности кода.
Более скорый способ увеличения индекса исходя из формулы первой модели "MI = 171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln (LOC) + 50 * sin(sqrt(2.4 * COM))" возможен за счёт увеличения объёма комментариев, так как индекс в формуле – наибольшее положительное число "50". Объём комментариев зависит от функции синуса, которая колеблется в пределах [-1..1]. Но поскольку объём комментариев можно исчислять двумя способами – как процент или долю, а значение синуса может быть и отрицательным, то необходимо выяснить оптимальные пределы комментариев. Для этого рассмотрим график функции "y(x)= 50 * sin(sqrt(2.4 * x))" на отрезках [0..100] (при исчислении комментариев в процентах) и [0..1] (при исчислении комментариев в долях от общего кода).
Рис.6 "График комментариев в процентах"
На графике (см. Рис.6 "График комментариев в процентах"), построенном по формуле "y(x)= 50 * sin(sqrt(2.4 * x))", по оси X имеем процентное соотношение комментариев к строкам кода, а по оси Y – величину для расчёта Maintainability Index. 1-2% или 25-26% или 83-84% комментариев дадут желаемый максимум (y=50), 0% или 4% или 17% или 37% или 66% комментариев абсолютно не влияют на Maintainability Index (y=0), 9% или 50-51% комментариев самым сильным образом (y=-50) отрицательно влияют на индекс ремонтопригодности. При этом MI может стать и отрицательной величиной. 
Рис.7 "График комментариев в долях"
На графике (см. Рис.7 "График комментариев в долях"), построенном по формуле "y(x)= 50 * sin(sqrt(2.4 * x))", по оси X имеем долевое соотношение комментариев к строкам кода, а по оси Y – величину для расчёта Maintainability Index. График показывает, что 3-4 комментированные строки кода из 10 рассматриваемых (или каждая третья) максимально помогают увеличить MI, то есть являются самым полезным соотношением.
Итак, если MI упал ниже 65, то по-быстрому его поднять можно вставкой комментариев в каждую третью строку.
Примечания:
- полезность/качество комментариев не рассчитывается формулой, поэтому не стоит забывать, что просто закомментированный код – это не полезные примечания;
- при добавлении комментариев соответственно увеличится и общее количество строк кода.

Шаг второй по повышению индекса устойчивости подпрограммы – снизить количество строк кода, потому что в формуле "MI = 171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln (LOC)" наибольший отрицательный индекс "16.2" у величины строк. На графике (см. Рис.8 "График и формула строк кода") видно, например, что 4500 строк уменьшат индекс на 136 пунктов из 171 возможного по формуле. Количество строк в подпрограмме определяется по спецсимволам текста, поэтому парсер обмануть не получится обычным размещением команд в одну строку файла. Единственный вариант – оптимизация кода за счёт объединения и выноса идентичных блоков кода в самостоятельные функции, которые будут рассматриваться как отдельные подпрограммы.
Примечания:
- не забывайте, что при этом увеличится количество параметров и скорее всего даже глобальных;
- блок-схемы, flowchart или UML диаграммы помогают вычленить повторяющиеся блоки хода подпрограммы.
 
 Рис.8 "График и формула строк кода"
Если пойти от обратного и принять в формуле MI(<=65) максимальные значения HV(=1000), CC(=10) и комментариев (sin[X]=1), то в одном юните желательно иметь не более 1500 строк (см. Рис.9 "График максимального количества строк кода и формула с учётом комментариев").
 Рис.9 "График максимального количества строк кода и формула с учётом комментариев"
То есть по формуле Maintainability Index с учётом комментариев мы вывели лимит для оптимального количества строк в подпрограмме: LOC<1500 mi="">=65.
Для формулы второй модели (без учёта комментариев) смотрите рисунок 10 "График максимального количества строк кода и формула без учёта комментариев".
 Рис.10 "График максимального количества строк кода и формула без учёта комментариев"
График по оси Y показывает MI, а LOC по оси X. Из чего следует, что в юните без комментариев для стабильно-устойчивого кода нужно иметь менее 50 строк (см. Рис.11 "Увеличенный график максимального количества строк кода для формулы без учёта комментариев").
 
 Рис.11 "Увеличенный график максимального количества строк кода для формулы без учёта комментариев"
Об иных подробностях строк кода читайте в статье "Source lines of code".

Третий шаг по повышению MI – это снижение сложности Халстеда из части "5.2 * ln(HV)". Максимальная величина Халстеда – 1000. Исходя из этого на графике (см. Рис.12 "График и формула Halstead Volume в рамках MI") видно, что сложность Халстеда максимально может уменьшить индекс ремонтопригодности на 36 пунктов.
 
 Рис.12 "График и формула Halstead Volume в рамках MI"
Все величины Халстеда рассчитываются по операторам и операндам. Напомню, что в выражении "a+b" оператором является плюс, а операндами – переменные "a" и "b". Длина программы по Халстеду – это сумма всех операторов и всех операндов. Словарь юнита он определил как сумму уникальных операторов и операндов. А помножив длину программы на логарифм словаря, Халстед получил объём подпрограммы (или ещё эту величину называют сложностью Халстеда):
HV = ( TNOpr + TNOpd ) * log2 ( DNOpr + DNOpd ),
где TNOpr – общее количество операторов, TNOpd – общее количество операндов, DNOpr – количество уникальных операторов, DNOpd  – количество уникальных операндов. Более подробно о величинах Халстеда читайте в статье "Halstead complexity measures".
Если принять, что на один оператор приходится два операнда, то график функции HV будет приблизительно, как на рисунке 13 "График и функция сложности Халстеда".
 Рис.13 "График и функция сложности Халстеда"
По оси Y смотрим значение HV, а по оси X выбираем количество операторов. Максимальному значению HV=1000 соответствует 45 операторов, и соответственно около 90 операндов (~ два операнда на один оператор).
Для увеличения MI до 65 и выше надо уменьшать HV, то есть количество операторов и операндов. В коде (см. пример на рисунке 14 "Пример сложных и простых вычислений") можно множество простых операций объединить в одну сложную. Это как в начальной школе по арифметике при имеющемся решении задачи в три-четыре действия выполнить расчёт одним действием.
 Рис.14 "Пример сложных и простых вычислений"
На скриншоте из ClearSQL (см. Рис.15 "Пример упрощения кода по HV, MI") показана разница в значениях HV для сложной "hv_1" и простой "hv_2" подпрограмм.
 Рис.15 "Пример упрощения кода по HV, MI"
Уменьшая количество операторов и операндов в коде снижается сложность Халстеда (HV), и как следствие увеличивается (улучшается) индекс ремонтопригодности (MI).

Четвёртый шаг по улучшению MI – снижение цикломатической сложности.
Томас Дж.Маккейб вывел формулу цикломатической сложности юнита как разность рёбер (edges) и узлов (junctions) с добавлением удвоенного количества компонент связности (coherences):
CC = EdgesJunctions + 2*Coherences
Максимальное значение величины Маккейба утверждено Американским Национальным Институтом Стандартов и Технологий (NIST), равно 10, но последнее время расширяют до 15. Нам тестировщикам это значение говорит о необходимом количестве юнит-тестов. Более подробно о цикломатической сложности читайте в статье "Cyclomatic complexity". Цикломатическая сложность снижает индекс ремонтопригодности максимально на 2-3 пункта, поэтому её рассматриваем в последнюю очередь. График (см. Рис.16 "График и формула цикломатической сложности в рамках MI") показывает часть "0.23 * CC" из формулы MI.
 
 Рис.16 "График и формула цикломатической сложности в рамках MI"
По оси X берём целочисленное значение Cyclomatic Complexity (max=10 or 15), по оси Y видим объём снижения (коэффициент – отрицательная величина "-0.23") MI.
Для улучшения MI нужно уменьшать CC, то есть "выпрямлять" ход программы. Но учтите, что иногда сложные условия (композиция нескольких AND и OR) рассчитывается как одно, то есть уменьшается количество узлов и рёбер, следовательно и само значение CC будет меньше.
Рис.17 "Пример кода цикломатической сложности"
В примере (см. Рис.17 "Пример кода цикломатической сложности") процедура threeinone имеет одно сложное условие (CC=4 или CC=2), а процедура oneinthree – три простых условия (СС=4 всегда).

Итак, для оценки качества кода достаточно вычислить только одну величину – Maintainability Index. В слабом юните исправления следует применять по очереди к комментариям кода, количеству строк, операторов и операндов, узлов и связей хода программы. Элементарных знаний математического анализа и школьного курса информатики достаточно специалисту, решившему тестировать код на любом языке программирования, при этом не углубляясь в синтаксис языка.

Полезные ссылки:
* Качественный анализ программного модуля на основе метрик кода
* Don M. Coleman, Dan Ash, Bruce Lowther, Paul W. Oman. Using Metrics to Evaluate Software System Maintainability. IEEE Computer 27(8), 1994, pp. 44-49.
* Paul Omand and Jack Hagemeister. “Metrics for assessing a software system’s maintainability”. Proceedings International Conference on Software Mainatenance (ICSM), 1992, pp 337-344.
* Paul W. Oman, Jack R. Hagemeister: Construction and testing of polynomials predicting software maintainability. Journal of Systems and Software 24(3), 1994, pp. 251-266.
* The Maintainability Index was introduced at the International Conference on Software Maintenance in 1992
* To date, MI is included in Visual Studio (since 2007), in the recent (2012) JSComplexity and Radon metrics reporters for Javascript and Python, and in older metric tool suites such as verifysoft.
* Think Twice Before Using the Maintainability Index
* P. Oman
* J. Hagemeister


2 комментария: