7. Метрики складності коду. Цикломатична та когнітивна складність коду. Що для вас чистий код?

Відео версія
(YouTube - для зворотнього зв'язку)

Hard skill відповідь на питання "Що для вас чистий код?"

Код має чіткі метрики які можна вирахувати.

Цикломатична складність

Почну з класичної - цикломатична складність. Це метрика розроблена ще у 80-х роках для визначення складності проєкту, конкретного класу, або функції.

Я спробую пояснити ну дуже простими словами, якщо вам не достатньо, то загугліть. Алгоритм цикломатичної складності будує граф, де кожне розгалуження програми оператори if, else, for, do-while , кожен switch-case варіант, try-catch і так далі це вершина. І рахує скільки ж шляхів можливо пройти від початку до кінця алгоритму. Кількість можливих шляхів і є показник складності. Чим він більший, тим складніша ваша програма.

Але мені ментально подобається інше визначення. Воно має більше практичний сенс. Цикломатична складність показує скільки тестів треба зробити, щоб на 100% покрити весь код. Дуже простий приклад, щоб покрити функцію ділення треба 2 тести перший передасть 2 звичайних числа. Другий передасть дільник рівний нулю і буде очікувати помилку.

division example
function division($numerator, $denominator) {
    if ($denominator === 0) {
        throw new Exception("Error: Division by zero is not allowed.");
    }
    
    return $numerator / $denominator;
}

Саме цей алгоритм і показує тест коверейдж вашої програми. І це дуже класний приклад який показує що покриття тестами на 100% не тестує код на 100%, а лише показує що в тестах брати участь усі рядки коду.

І саме цикломатична складність налаштувалася автоматично у минулому відео про PHP Insights. Там звісно метрика трохи прихована і показує який відсоток класів має складність вищу за 5 за замовчуванням. Тут важливо розглядається кожен клас окремо, не вся програма разом.

Я звісно раджу його використовувати, і використовую сам. Дуже допомагає побачити що щось ти робиш не так... занадто складно.

Але у цикломатичної складності є проблема. Спробую її зобразити.

Ciclomatic complexity 5
for ($i = 0; $i < count($array); $i++) { // +1
    for ($j = 0; $j < count($array[$i]); $j++) { // +1
        for ($k = 0; $k < count($array[$i][$j]); $k++) { // +1
            for ($p = 0; $p < count($array[$i][$j][$k]); $p++) { // +1
                if ($array[$i][$j][$k][$p] === 1) { // +1
                    $result = 2;
                } 
            }
        }   
    }   
}

Це приклад коду з цикломатичною складність 5. Не велика складність. А ми тут розбираємо 4 мірний масив і робимо це в лоб. Будь-який розробник скаже що це капець складний код і мати рацію. А на противагу гляньте на цей код

Also ciclomatic complexity 5
switch($number) {
    case 1: return 'One'; // +1
    case 2: return 'Two'; // +1
    case 3: return 'Tree'; // +1
    case 4: return 'Four'; // +1
    default: return 'More'; // +1
}

І це також складність 5. Не дуже складно погодьтесь.

Тож робимо висновок. Цикломатична складність корисна, але не завжди показує реальну складність.

Когнітивна складність

І знаючи цю проблему у 2017 році компанія sonar (sonarqube) представила свою метрику.

Когнітивна складність. В основі це та ж цикломатична складність, але, по-перше, кожен вложений оператор додає на одиницю складності більше. Тобто приклад з 4-х мірним масивом має складність 15.

Cognitive complexity 15
for ($i = 0; $i < count($array); $i++) { // +1
    for ($j = 0; $j < count($array[$i]); $j++) { // +2
        for ($k = 0; $k < count($array[$i][$j]); $k++) { // +3
            for ($p = 0; $p < count($array[$i][$j][$k]); $p++) { // +4
                if ($array[$i][$j][$k][$p] === 1) { // +5
                    $result = 2;
                } 
            }
        }   
    }   
}

По друге когнітивна складність враховує конструкції мови. Тернарні оператори, match, switch-case й так далі. Тому складність прикладу зі switch-case буде рівнятися 1.

Почитати офіційну документацію про когнітивну складність можна тут.

Звісно є інші метрики, наприклад глибина наслідування, або рівень зв'язаності класів.

Але я тут більше по практичній частині. Тож цикломатична складність уже налаштована, і я маю з нею досвід, дуже раджу.

Cognitive complexity setup

З когнітивною складністю ще не працював, але дуже хочу. Тому ставлю пакет Він вимагає phpstan, але це того варте.

І власне конфіг

phpstan.neon
parameters:
    level: 0
    paths:
        - app/
        - tests/
    cognitive_complexity:
        class: 50
        function: 8
  • level: 0 - рівень перевірки PHPStan. Ставлю 0 (найслабший) оскільки саме код правки та аналіз робить PHP Insights. Дублювати перевірку не потрібно.
  • paths - директорії які потрібно сканувати, сюди також додам модулі
  • cognitive_complexity - максимально допустима когнітивна складність для класу та конкретної функції.

І додаємо команду до .husky/pre-commit та gitlab-ci.yml

.husky/pre-commit
docker exec -t bh-app php artisan insights --fix --no-interaction
docker exec -t bh-app vendor/bin/phpstan
git add $(git diff --cached --name-only --diff-filter=ACM)

gitlab-ci.yml -> insights
  script:
    - docker compose exec app php artisan insights --no-interaction
    - docker compose exec app vendor/bin/phpstan