Explain cycle complexity

Explain cycle complexity

Cycle complexity concept

Cyclomatic complexity (CC) is also known as conditional complexity, which is a measure of code complexity. It was proposed by Thomas J. McCabe (Sr.) in 1976 to represent the complexity of the program, and its symbol is VG or M. It can be used to measure the complexity of a module decision structure. The number is expressed as the number of independent current paths, and can also be understood as the number of test cases that cover all possible situations and use the least. The large circle complexity indicates that the judgment logic of the program code is complex, the quality may be low, and it is difficult to test and maintain. The possible errors of the program are closely related to the high cycle complexity.

Cycle complexity calculation method

Point edge calculation method

The calculation method of cycle complexity is very simple, and the calculation formula is:

V(G) = E - N + 2

Where e represents the number of edges in the control flow graph and n represents the number of nodes in the control flow graph.

Several nodes are connected by edges. The following are typical control processes, such as if else, While, until and normal process sequence:

Node decision method

In fact, there is a more intuitive method to calculate the cycle complexity. Because the cycle complexity reflects the number of "decision conditions", the cycle complexity is actually equal to the number of decision nodes plus 1, that is, the number of areas of the control flow graph. The corresponding calculation formula is:

V (G) = P + 1

Where P is the number of decision nodes, for example:

  1. if statement
  2. while statement
  3. for statement
  4. case statement
  5. catch statement
  6. And and or Boolean operations
  7. ?: Ternary operator

For multi branch CASE structure or if-else if-else structure, special attention should be paid to when counting the number of decision nodes. It is required to count all the actual number of decision nodes, that is, each else if statement and each CASE statement should be counted as a decision node.

The decision node can be easily identified in the control flow diagram of the module. Therefore, when calculating the cycle complexity V(G) for the control flow diagram of the program, the point edge calculation method is generally adopted, that is, V(G)=e-n+2; For the control flow diagram of the module, statistics can be directly used to determine the number of nodes, which is simpler.

Circle complexity calculation exercise

Exercise 1:

void sort(int * A)
{
    int i=0;
   int n=4;
   int j = 0;
   while(i < n-1)
   {
       j = i +1
       while(j < n)
       {
           if (A[i] < A[j])
                swap(A[i], A[j]);
       }
       i = i + 1
   }
}

Draw the control flow diagram using the point edge calculation method:

Its cycle complexity is: V(G) = 9 - 7 + 2 = 4

Exercise 2:

U32 find (string match){
         for(auto var : list)
         {
             if(var == match && from != INVALID_U32) return INVALID_U32;
         }
         //match step1
         if(session == getName() && key == getKey())
         {
             for (auto& kv : Map)
             {
                 if (kv.second == last && match == kv.first)
                 {
                     return last;
                 }
             }

         }
         //match step2
         auto var = Map.find(match);
         if(var != Map.end()&& (from != var->second)) return var->second;

         //match step3
         for(auto var: Map)
         {
             if((var.first, match) && from != var.second)
             {
                 return var.second;
             }
         }
         return INVALID_U32;
     };

Its cycle complexity is: V (g) = 1 (for) + 2 (if) + 2 (if) + 1 (for) + 2 (if) + 2 (if) + 2 (if) + 1 (for) + 2 (if) + 1 = 14

Significance of cycle complexity

Capture defects before they become defects.

Cycle complexity and defects

Generally speaking, methods with cyclomatic complexity greater than 10 have a great risk of error. There is a high positive correlation between cycle complexity and the number of defects: the modules and methods with the highest cycle complexity may also have the largest number of defects.

Cycle complexity and structured testing

In addition, it also provides a good reference for test design. A good use case design experience is to create a number of test cases equal to the complexity value of the tested code circle, so as to improve the branch coverage of the use case to the code.

Cycle complexity and TDD

There is a close relationship between TDD (Test Driven Development) and low CC value. When writing tests, developers will consider the testability of the code and tend to write simple code because complex code is difficult to test. Therefore, the "code, test, code, test" cycle of TDD will lead to frequent refactoring and drive the development of non complex code.

Cyclomatic complexity and legacy code

For the maintenance or refactoring of legacy code, measuring cycle complexity is particularly valuable. Generally, cycle complexity is used as an entry point to improve code quality.

Cycle complexity and CI

In a continuous integration environment, the complexity and growth value of a module or function can be evaluated based on the time-varying dimension. If the CC value is growing, two activities should be carried out:

  1. Ensure the effectiveness of relevant tests and reduce the risk of failure.
  2. Evaluate the necessity and specific ways of refactoring to reduce the possibility of code maintenance problems.

Cycle complexity and software quality

Cycle complexity

Code status

Testability

Maintenance cost

1-10

Clear and structured

high

low

10-20

complex

in

in

20-30

Very complicated

low

high

>30

unreadable

Immeasurable

Very high

Methods of reducing cycle complexity

Reorganize your functions

Tip 1 refining functions

There is a piece of code that can be organized and separated:

void Example(int val)
{
    if( val > MAX_VAL)
    {
        val = MAX_VAL;
    }

    for( int i = 0; i < val; i++)
    {
        doSomething(i);
    }
}

Put this code into a separate function and let the function name explain the purpose of the function:

int getValidVal(int val)
{
       if( val > MAX_VAL)
    {
        return MAX_VAL;
    } 
    return val;
}

void doSomethings(int val)
{
    for( int i = 0; i < val; i++)
    {
        doSomething(i);
    }
}

void Example(int val)
{
    doSomethings(getValidVal(val));
}

Finally, we need to re-examine whether the function content is at a unified level.

Tip 2 replacement algorithm

Replace one algorithm with a clearer algorithm:

string foundPerson(const vector<string>& peoples){
  for (auto& people : peoples) 
  {
    if (people == "Don"){
      return "Don";
    }
    if (people == "John"){
      return "John";
    }
    if (people == "Kent"){
      return "Kent";
    }
  }
  return "";
}

Replace the function implementation with another algorithm:

string foundPerson(const vector<string>& people){
  std::map<string,string>candidates{
        { "Don", "Don"},
        { "John", "John"},
        { "Kent", "Kent"},
       };
  for (auto& people : peoples) 
  {
    auto& it = candidates.find(people);
    if(it != candidates.end())
        return it->second;
  }
}

The so-called table driven.

Simplified conditional expression

Technique 3 reverse expression

Conditions that may exist in the code are expressed as follows:

if ((condition1() && condition2()) || !condition1())
{
    return true;
}
else
{
    return false;
}

After applying reverse expression to change the expression order, the effect is as follows:

if(condition1() && !condition2())
{
    return false;
}

return true;

Skill 4 decomposition conditions

There are complex conditional expressions in the code:

if(date.before (SUMMER_START) || date.after(SUMMER_END))
    charge = quantity * _winterRate + _winterServiceCharge;
else 
    charge = quantity * _summerRate;

Separate functions are extracted from the if, then and else paragraphs:

if(notSummer(date))
    charge = winterCharge(quantity);
else 
    charge = summerCharge (quantity);

Tip 5 merge conditions

The same result is obtained from a series of conditional judgments:

double disabilityAmount() 
{
    if (_seniority < 2) return 0;
    if (_monthsDisabled > 12) return 0;
    if (_isPartTime) return 0;
    // compute the disability amount
    ......

Combine these judgments into a conditional expression and refine the conditional expression into an independent function:

double disabilityAmount() 
{
    if (isNotEligableForDisability()) return 0;
    // compute the disability amount
    ......

Tip 6 remove control marks

In code logic, bool type is sometimes used as logic control flag:

void checkSecurity(vector<string>& peoples) {
    bool found = false;
    for (auto& people : peoples) 
    {
        if (! found) {
            if (people == "Don"){
                sendAlert();
                found = true;
            }
            if (people == "John"){
                   sendAlert();
                   found = true;
            }
        }
    }
}

Replace the control flag with break and return:

void checkSecurity(vector<string>& peoples) {
    for (auto& people : peoples)
    {     
        if (people == "Don" || people == "John")
        {
            sendAlert();
            break;
        }
    }
}

Technique 7 replacing conditional expressions with polymorphisms

Conditionals select different behaviors according to different object types:

double getSpeed() 
{
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
    throw new RuntimeException ("Should be unreachable");
}

Put each branch of the entire conditional expression into the overloaded method of a subclass, and then declare the original function as an abstract method:

class Bird
{
public:
    virtual double getSpeed() = 0;

protected:
    double getBaseSpeed();
}

class EuropeanBird
{
public:
    double getSpeed()
    {
        return getBaseSpeed();
    }
}

class AfricanBird
{
public:
    double getSpeed()
    {
        return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
    }

private:
    double getLoadFactor();

    double _numberOfCoconuts;
}

class NorwegianBlueBird
{
public:
    double getSpeed()
    {
        return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    };

private:
    bool _isNailed;
}

Simplify function calls

Skill 8 separation of reading and writing

A function not only returns the object state value, but also modifies the object state:

class Customer
{
    int getTotalOutstandingAndSetReadyForSummaries(int number);
}

Create two different functions, one for querying and the other for modifying:

class Customer
{
    int getTotalOutstanding();
    void SetReadyForSummaries(int number);
}

Skill 9 parametric method

Several functions do similar work, but they contain different values in the function body:

Dollars baseCharge()
 {
    double result = Math.min(lastUsage(),100) * 0.03;
    if (lastUsage() > 100)
    {
        result += (Math.min (lastUsage(),200) - 100) * 0.05;
    }
    if (lastUsage() > 200)
    {
        result += (lastUsage() - 200) * 0.07;
    }
    return new Dollars (result);
}

Establish a single function to express those different values with parameters:

Dollars baseCharge() 
{
    double result = usageInRange(0, 100) * 0.03;
    result += usageInRange (100,200) * 0.05;
    result += usageInRange (200, Integer.MAX_VALUE) * 0.07;
    return new Dollars (result);
}

int usageInRange(int start, int end) 
{
    if (lastUsage() > start) 
        return Math.min(lastUsage(),end) -start;

    return 0;
}

Tip 10 replace parameters with explicit functions

The function implementation depends entirely on the parameter value and takes different reactions:

void setValue (string name, int value) 
{
    if (name == "height")
        _height = value;
    else if (name == "width")
        _width = value;
    Assert.shouldNeverReachHere();
}

Establish an independent function for each possible value of the parameter:

void setHeight(int arg) 
{
    _height = arg;
}
void setWidth (int arg) 
{
    _width = arg;
}

Actual combat practice

Take the example of CC value statistics before:

 U32 find (string match){
         for(auto var : List)
         {
             if(var == match && from != INVALID_U32) 
            return INVALID_U32;
         }
         //match step1
         if(session == getName() && key == getKey())
         {
             for (auto& kv : Map)
             {
                 if (kv.second == last && match == kv.first)
                 {
                     return last;
                 }
             }

         }
         //match step2
         auto var = Map.find(match);
         if(var != Map.end()&& (from != var->second)) return var->second;

         //match step3
         for(auto var: Map)
         {
             if((var.first, match) && from != var.second)
             {
                 return var.second;
             }
         }
         return INVALID_U32;
     };

After comprehensively applying the techniques of reducing CC value:

namespace
{
    struct Matcher
    {
        Matcher(string name, string key);
        U32 find();

    private:
        bool except();
        U32 matchStep1();
        U32 matchStep2();
        U32 matchStep3();

        bool isTheSameMatch();

        string match;
        U32 from;
    };

    Matcher::Matcher(string name, string key):
        match(name + key)
    {
        from = GetFrom();
    }

    U32 Matcher::find()
    {
        if (except())
            return INVALID_U32;

        auto result = matchStep1();
        if (result != INVALID_U32)
            return result;

        result = matchStep2();
        if (result != INVALID_U32)
            return result;

        return matchStep3();
    }

    bool Matcher::except()
    {
        for(auto var : List)
        {
            if(var == match && from != INVALID_U32)
                return true;
        }

        return false;
    }

    U32 Matcher::matchStep1()
    {
        if(!isTheSameMatch())
        {
            return INVALID_U32;
        }

        for (auto& kv : Map)
        {
            if ( last == kv.second && match == kv.first)
            {
                return last;
            }
        }

        return INVALID_U32;
    }

    bool Matcher::isTheSameMatch()
    {
        return match == getName() + getKey();
    }

    U32 Matcher::matchStep2()
    {
        auto var = Map.find(match);
        if(var != Map.end()&& (from != var->second))
        {
            return var->second;
        }

        return INVALID_U32;
    }

    U32 Matcher::matchStep3()
    {
        for(auto var: Map)
        {
            if(keyMatch(var.first, match) && from != var.second)
            {
                return var.second;
            }
        }

        return INVALID_U32;
    }
}

U32 find (string match)
{
    Matcher matcher;

    return matcher.find(match);
}

In this example, the matching algorithms are encapsulated in the Matcher class, and the original logic is abstracted into four steps: capability query, stickiness, exact matching and fuzzy matching through refining function (skill 1) and merging conditions (skill 6). In this way, loops and conditional branches are enclosed in small functions, so as to reduce the interface function (findPno) The overall cycle complexity is reduced from 14 of a single function to 5 of multiple functions.

Speculation on cycle complexity

Speculation 1 is the maintainability of high complexity code poor

In actual projects, for the convenience of debugging, the name corresponding to the message number is often printed:

string getMessageName(Message msg)
{
    switch(msg)
    {
        case MSG_1:
            return "MSG_1";
        case MSG_2:
            return "MSG_2";
        case MSG_3:
            return "MSG_3";
        case MSG_4:
            return "MSG_4";
        case MSG_5:
            return "MSG_5";
        case MSG_6:
            return "MSG_6";
        case MSG_7:
            return "MSG_7";
        case MSG_8:
            return "MSG_8";
        default:
            return "MSG_UNKNOWN"
    }
}

This code is acceptable in terms of readability and maintainability. Therefore, when refactoring is performed because of "high" complexity (for example, skill 2 or skill 6), it will reduce the cycle complexity and bring unnecessary logical complexity.

Of course, it is necessary to further reduce the cycle complexity if:

  1. Too many messages.
  2. switch... Case... Multiple repetitions. When there are too many messages, you can consider classifying the messages and then refactoring with technique 1. In the case of multiple repetitions, you can gather the contents of the same case into a specific class method through technique 6, and then use it in a polymorphic way.

Consider 2 whether the code with the same complexity is consistent

For example, the cyclomatic complexity of the following two code fragments is 6. Code snippet 1:

string getWeight(int i) {
        if (i <= 0) 
        {
                return "no weight";
        }
        if (i < 10) 
        {
                return "light";
        }
        if (i < 20) 
        {
                return "medium";
        }
        if (i < 30) 
        {
                return "heavy";
        }
        if (i < 40)
        {
            return "very heavy";
        }

        return "super heavy"
}

Code snippet 2

int sumOfNonPrimes(int limit) {
        bool bAdd = false;
        int sum = 0;
        for (int i = 0; i < limit; ++i) {
                if (i <= 2) 
                    continue;

                for (int j = 2; j < i; ++j) 
                {
                    if (i % j == 0) 
                    {
                            bAdd = false;
                            break;
                    }
                    bAdd = true;
                }
                if (bAdd)
                    sum += i;
        }
        return sum;
}

However, in terms of readability and maintainability, code fragment 1 should be better than code fragment 2, and code fragment 2 has a stronger bad taste. Therefore, the cycle complexity needs to be analyzed in specific situations, which can only be used as a measure of reconstruction and a reference basis for decision-making.

Cyclomatic complexity tool

There are many tools for circle complexity, which can be divided into three categories:

type

name

explain

Special tools (single language)

OCLint

C language related

GMetrics

Java

PyMetrics

python

JSComplexity

js

General tools (Multilingual)

lizard

Support multiple languages: C/C++ (works with C++14), Java, c#, JavaScript, Objective C, Swift, Python, Ruby, PHP, Scala, etc.

sourcemonitor

Free, Windows platform. Supported languages include C, C + +, c#, Java, VB, Delphi and HTML.

General platform

sonarqube

An open source platform for code quality management, supporting more than 20 languages. Different testing tools, code analysis tools and continuous integration tools can be integrated through plug-in mechanism

source: //kaelzhang81.github.io/2017/06/18/explain cycle complexity in detail

Posted on Thu, 11 Nov 2021 23:19:55 -0500 by citricsquid