Цикломатическая сложность в фрагменте кода с несколькими точками выхода

У меня есть метод проверки пароля:

/**
 * Checks if the given password is valid.
 * 
 * @param password The password to validate.
 * @return {@code true} if the password is valid, {@code false} otherwise.
 */
public static boolean validatePassword(String password) {
    int len = password.length();
    if (len < 8 || len > 20)
        return false;
    boolean hasLetters = false;
    boolean hasDigits = false;
    for (int i=0; i<len; i++) {
        if (!Character.isLetterOrDigit(password.charAt(i)))
            return false;
        hasDigits = hasDigits || Character.isDigit(password.charAt(i));
        hasLetters = hasLetters || Character.isLetter(password.charAt(i));
    }
    return hasDigits && hasLetters;
}

Сосредоточимся на цикломатическом числе сложности: в чем его значение?

Metrics 1.3.6 говорит, что это 7, но я не могу найти семь независимых путей: я нашел только 5! И Википедия не сильно помогла, как я могу использовать эту формулу π - s + 2?

У меня есть 2 if, 1 for и 3 точки выхода, но я застрял: нужно ли мне считать точку входа? Следует ли мне дважды считать первый if, поскольку он имеет два условия?

РЕДАКТИРОВАТЬ:

Хорошо, теперь я узнал, что Цикломатический номер равен 7. Это означает, что существует 7 независимых путей, и поэтому я смогу найти 7 различных тестовых примеров, если бы я хотел покрыть 100% кода, я прав?

Что ж, я все еще не могу найти последнюю! Я нашел вот такие:

  1. Действителен: asdf1234
  2. Слишком коротко: asdf123
  3. Слишком долго: asdfsgihzasweruihioruldhgobaihgfuiosbhrbgtadfhsdrhuorhguozr
  4. Недействительный символ: asdf * 123
  5. Все цифры: 12345678
  6. Без цифр: asdfghjk
  7. wtf ???

person kelo    schedule 13.03.2013    source источник
comment
Посмотрите на нормализованный код в моем ответе. После учета короткого замыкания || и && у вас есть 7 операторов ветвления   -  person Claudiu    schedule 13.03.2013
comment
7-я ветвь - это когда цикл for завершается. Один путь входит в цикл, другой - нет. У вас никогда не бывает случая, чтобы цикл for не выполнялся в вашем коде, потому что вы проверяете len < 8 в начале своей функции, но простой автоматический анализ кода не отразит это.   -  person Claudiu    schedule 13.03.2013
comment
@Claudiu: Я думал, что это могло быть так, но я не был уверен ... я сомневаюсь: почему for должны считаться дважды, где if и все остальное считается только один раз? Даже оператор if имеет два пути, но увеличивает ccn только на единицу, и поэтому должен делать и for. : |   -  person kelo    schedule 13.03.2013
comment
Я не думаю, что у вас на самом деле будет 7 разных тестовых примеров, потому что некоторые из ваших точек принятия решения фактически проверяют одно и то же: если !isLetterOrDigit истинно, то мы знаем, что либо isDigit, либо isLetter будет истинным.   -  person matts    schedule 13.03.2013
comment
@tmh: for не считается дважды. У вас есть шесть if операторов и один loop.   -  person Claudiu    schedule 13.03.2013
comment
@matts: спасибо! Вот и все! Как я был глуп!   -  person kelo    schedule 13.03.2013
comment
как я могу использовать эту формулу π - s + 2 == ›Это можно сделать, когда вы создаете граф потока управления вашего кода, где π - количество узлов, s - количество ребер. Вы можете найти множество инструментов, которые могут автоматически генерировать поток графа.   -  person Aziz    schedule 22.11.2017


Ответы (3)


Думаю, хитрость в том, что подсчитываются логические операторы.

На основе вашей ссылки Metrics (http://metrics.sourceforge.net/) в разделе McCabe Cyclomatic Complexity :

1 Начальный поток

3 точки принятия решения (если, за, если)

3 условных логических оператора (||, ||, ||)

всего: 7

person Shellum    schedule 13.03.2013
comment
кстати, логические операторы считаются ветвями, потому что они имеют поведение при коротком замыкании. например, когда выражение в левой части оператора || оценивается как true, правая часть не выполняется. - person matts; 13.03.2013
comment
Думаю, ты прав, но мне интересно ... это правильно? Итак, каков реальный цикломатический номер этого фрагмента кода? Правильны ли метрики? Смысл всего этого в том, чтобы охватить все возможные пути тестирования. РЕДАКТИРОВАТЬ: подумав дважды, я обнаружил, что показатели верны; Я не рассматривал два тестовых случая: полностью цифровой пароль и пароль без цифр. Кстати, я думаю, что последний && не увеличивает ccn: вы не посчитали основной поток :) - person kelo; 13.03.2013
comment
Ты прав. Ответ отредактирован, чтобы показать это. Похоже, есть много способов подсчета, которые обычно приводят к одному и тому же результату: stackoverflow .com / questions / 10365912 / - person Shellum; 13.03.2013
comment
Я отредактировал свой ответ, не могли бы вы взглянуть? :) Возможно ты можешь помочь мне :) - person kelo; 13.03.2013
comment
Возможно, что два пути могут быть объединены, даже если сложность дает определенное число. Например, если у вас есть if (SOME_CONSTANT) {}, это будет точка принятия решения, но она будет проходить только через одну из двух ветвей, потому что она основана на константе. Точно так же, если вы не можете найти 7-й тестовый пример, это может быть потому, что он уже продублирован в предыдущем. - person Shellum; 13.03.2013

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

public static boolean validatePassword(String password) {
    int len = password.length();

    //evaluate 'len < 8 || len > 20'
    bool cond1 = len < 8;
    if (!cond1) cond1 = len > 20;
    //do the if test
    if (cond1)
        return false;

    boolean hasLetters = false;
    boolean hasDigits = false;
    //for loops are equivalent to while loops
    int i = 0;
    while(i < len) {
        if (!Character.isLetterOrDigit(password.charAt(i)))
            return false;

        //evaluate 'hasDigits || Character.isDigit(password.charAt(i))'
        bool hasDigitsVal = hasDigits;
        if (!hasDigitsVal) hasDigitsVal = Character.isDigit(password.charAt(i));
        //hasDigits = ...
        hasDigits = hasDigitsVal

        //evaluate 'hasLetters || Character.isLetter(password.charAt(i))'
        bool hasLettersVal = hasLetters;
        if (!hasLettersVal) hasLettersVal = Character.isLetter(password.charAt(i));
        //hasLetters = ...
        hasLetters = hasLettersVal;

        i++;
    }

    //evaluate 'hasDigits && hasLetters'
    bool cond2 = hasDigits;
    if (cond2) cond2 = hasLetters;
    //return ...
    return cond2;
}

Обратите внимание, что операторы || и && по сути просто добавляют в код операторы if. Также обратите внимание, что теперь у вас есть 6 операторов if и один цикл while! Может быть, это та семерка, которую вы искали?


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

void foo() {
    if (cond1) return a;
    if (cond2) return b;
    return c;
}

График будет выглядеть так, где -----val----> EXIT означает выход из функции со значением val:

START -> cond1 ------------------------a------------> EXIT
           |                                            |
         cond2 ------------------------b----------------+
           |                                            |
         return -----------------------c----------------|

Если вы переписываете код, вы просто добавляете еще один узел "pre-return", который затем переходит к узлу выхода:

void foo() {
    int val;
    if (cond1) {
        val= a;
    }
    else {
        if (cond2) {
            val= b;
        }
        else {
            val= c;
        }
    }
    return val;
}

Теперь это выглядит так:

START -> cond1 ---> val=a --------------------------> return ----val----> EXIT
           |                                            |
         cond2 ---> val=b ------------------------------+
           |                                            |
           + -----> val=c ------------------------------+

Он по-прежнему сложен, а код просто уродливее.

person Claudiu    schedule 13.03.2013
comment
Я бы не сказал, что у него несколько точек выхода, что вы имеете в виду? Раньше я думал, что каждый раз, когда вы возвращаетесь, вы добавляете точку выхода (например, if (somecondition) return;) - person kelo; 13.03.2013
comment
@tmh: я обновил свой ответ, чтобы показать, что я имею в виду. думать о нескольких return заявлениях - отвлекающий маневр. ИМО - person Claudiu; 13.03.2013

Как красиво объяснено здесь:

Цикломатическая сложность = (2 + ifs + loop + case - return), где:

* ifs is the number of IF operators in the function,
* loops is the number of loops in the function,
* cases is the number of switch branches in the function (without default), and
* return is the number of return operators in the function.

Как уже было сказано, вычисляются и логические условия.

Например, if (len < 8 || len > 20) считается за 3 условия:

  1. if
  2. len<8
  3. len > 20

Это означает, что ваш код имеет сложность 2 + 8 - 3 = 7, где:

  • 2 - он всегда там (см. Формулу наверху)
  • 8 - количество веток
  • 3 - количество возвратов
person BЈовић    schedule 09.12.2013