Блог о программировании

Цикломатическая сложность

Категория: DevOps
 25 июня 2017 г. 21:44

Цикломатическая сложность (Cyclomatic Complexity Number, ccn) программного кода является одной из наиболее старых метрик. Впервые эта метрика была упомянута в 1976 году Томасом МакКэбом. Эта метрика подсчитывает доступные пути выполнения кода во фрагменте программного обеспечения, чтобы определить его сложность. Каждый путь выполнения включает одну условную конструкцию из приведенного ниже списка, так что это их довольно легко обнаружить в существующем исходном коде:

  • ?
  • case
  • elseif
  • for
  • foreach
  • if
  • while

В данном случае нет конструкций else и default, потому что они в любом случае предполагают использование конструкций if и case, которые присутствуют в списке.

Каждый путь выполнения увеличивает сложность на 1, вычисляя тем самым цикломатическую сложность анализируемого фрагмента кода. Обратите внимание, что каждая функция и метод также имеет значение 1. Проще всего понять данную концепцию на примере. Вычислим цикломатическую сложность следующего фрагмента кода:

<?php
//                                                         | CCN
// -------------------------------------------------------------
class CyclomaticComplexityNumber                        // |  0
{                                                       // |  0
    public function example( $x, $y )                   // |  1
    {                                                   // |  0
        if ( $x > 23 || $y < 42 )                       // |  1
        {                                               // |  0
            for ( $i = $x; $i >= $x && $i <= $y; ++$i ) // |  1
            {                                           // |  0
            }                                           // |  0
        }                                               // |  0
        else                                            // |  0
        {                                               // |  0
            switch ( $x + $y )                          // |  0
            {                                           // |  0
                case 1:                                 // |  1
                    break;                              // |  0
                case 2:                                 // |  1
                    break;                              // |  0
                default:                                // |  0
                    break;                              // |  0
            }                                           // |  0
        }                                               // |  0
    }                                                   // |  0
    file_exists('/tmp/log') or touch('/tmp/log');       // |  0
}                                                       // |  0
// -------------------------------------------------------------
//  

Основываясь на описании выше, цикломатическая сложность предоставленного фрагмента кода равна 5. Однако, внимательный читатель мог бы заметить, что в подсчете цикломатической сложности не принимают участие операторы || и && на строке 8 и 10, а также or на строке 27. Существует более современная разновидность цикломатической сложности, учитывающая данные операторы (и, соответственно, новые пути выполнения кода), аббревиатура ей ccn2. Ccn2 является более распространенной метрикой ПО. Множество утилит, такие как PHPUnit, PHP Mess Detector и другие под цикломатической сложностью понимают именно ccn2.

С новыми вводными цикломатическая сложность увеличилась до 8, то есть сложность выросла аж на 60%!

Ввиду того, что цикломатическая сложность была придумана для процедурных языков программирования, данная метрика до сих пор не учитывает один элемент сложности в объектно-ориентированном программировании. С появлений таких понятий как исключения программа получает дополнительные пути выполнения для каждого оператора catch, используемого в коде, поскольку внутри оператора try описывается поведение программы по-умолчанию.

  • ?
  • &&
  • ||
  • or
  • and
  • xor
  • case
  • catch
  • elseif
  • for
  • foreach
  • if
  • while

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

  • 1-4 — это низкая цикломатическая сложность фрагмента кода;
  • 5-7 — данная сложность вполне управляема, и фрагмент кода все еще прост для понимания;
  • 6-10 — данная величина указывает на возможную сложность в понимании фрагмента кода;
  • > 10 — фрагмент сложен для понимания.

Может появиться вопрос, зачем заморачиваться за эту цикломатическую сложность? Какую выгоду этот параметр может мне принести?

Наиболее сложные части приложения обычно содержат критическую бизнес-логику. Так вот, высокая цикломатическая сложность негативно сказывается на понимании такой бизнес-логики, на читабельности и осмыслении исходников. Такие сложные части обычно являются рассадником всевозможных багов, превращаясь в кошмар для программиста, поддерживающего такой код. Все это выливается в идеологию «Работает — не трогай!», что в дальнейшем трансформируется в большое количество копи-паст кода. Таким образом, код приходит в негодность. Особенно обостряется ситуация, когда программист, долго обслуживающий этот код, и, фактически, единственный его понимающий, увольняется из компании.

Ну и напоследок наглядный пример, как высокая цикломатическая сложность негативно влияет на понимании кода, а также на процессе разработки в целом. Код ниже — сложный метод из исходников утилиты PHPDepend. Этот метод имеет цикломатическую сложность 16, что вызывает сложность в его понимании даже у автора данного метода.

<?php
// ...
private function _countCalls(PHP_Depend_Code_AbstractCallable $callable)
{
    $callT  = array(
        \PDepend\Source\Tokenizer\Tokens::T_STRING,
        \PDepend\Source\Tokenizer\Tokens::T_VARIABLE
    );
    $chainT = array(
        \PDepend\Source\Tokenizer\Tokens::T_DOUBLE_COLON,
        \PDepend\Source\Tokenizer\Tokens::T_OBJECT_OPERATOR,
    );

    $called = array();

    $tokens = $callable->getTokens();
    $count  = count($tokens);
    for ($i = 0; $i < $count; ++$i) {
        // break on function body open
        if ($tokens[$i]->type === \PDepend\Source\Tokenizer\Tokens::T_CURLY_BRACE_OPEN) {
            break;
        }
    }

    for (; $i < $count; ++$i) {
        // Skip non parenthesis tokens
        if ($tokens[$i]->type !== \PDepend\Source\Tokenizer\Tokens::T_PARENTHESIS_OPEN) {
            continue;
        }
        // Skip first token
        if (!isset($tokens[$i - 1]) || !in_array($tokens[$i - 1]->type, $callT)) {
            continue;
        }
        // Count if no other token exists
        if (!isset($tokens[$i - 2]) && !isset($called[$tokens[$i - 1]->image])) {
            $called[$tokens[$i - 1]->image] = true;
            ++$this->_calls;
            continue;
        } else if (in_array($tokens[$i - 2]->type, $chainT)) {
            $identifier = $tokens[$i - 2]->image . $tokens[$i - 1]->image;
            for ($j = $i - 3; $j >= 0; --$j) {
                if (!in_array($tokens[$j]->type, $callT)
                    && !in_array($tokens[$j]->type, $chainT)
                ) {
                    break;
                }
                $identifier = $tokens[$j]->image . $identifier;
            }

            if (!isset($called[$identifier])) {
                $called[$identifier] = true;
                ++$this->_calls;
            }
        } else if ($tokens[$i - 2]->type !== \PDepend\Source\Tokenizer\Tokens::T_NEW
            && !isset($called[$tokens[$i - 1]->image])
        ) {
            $called[$tokens[$i - 1]->image] = true;
            ++$this->_calls;
        }
    }
}

А так выглядит метод, выполняющий ту же самую работу, но подвергшийся рефакторингу:

<?php
// ...
private function _countCalls(PHP_Depend_Code_AbstractCallable $callable)
{
    $called = array();

    $tokens = $callable->getTokens();
    $count  = count($tokens);
    for ($i = $this->_findOpenCurlyBrace($tokens); $i < $count; ++$i) {

        if ($this->_isCallableOpenParenthesis($tokens, $i) === false) {
            continue;
        }

        if ($this->_isMethodInvocation($tokens, $i) === true) {
            $image = $this->_getInvocationChainImage($tokens, $i);
        } else if ($this->_isFunctionInvocation($tokens, $i) === true) {
            $image = $tokens[$i - 1]->image;
        } else {
            $image = null;
        }

        if ($image !== null) {
            $called[$image] = $image;
        }
    }

    $this->_calls += count($called);
}

Читабельность сильно зависит от сложности и количества управляющих последовательностей. Как мы видим, новая версия метода куда более проста для понимания, нежели старая. Получившаяся цикломатическая сложность — 5.

Стоит заметить, что перед рефакторингом необходимо убедиться, что имеющиеся юнит-тесты достаточно хороши для того, чтобы проверить работоспособность подвергаемого рефакторингу кода. В противном случае, проводить рефакторинг крайне затруднительно.

Поделиться статьей

Оставить комментарий