Probably, the conditional blocks, or in a more informal way, the “Ifs”, are some of the most widely used tools in programming. The truth is that they have been an integral component since the first programming languages, and although some details have been added to them, they are still practically the same as those of distant times. Throughout the evolution of C# new options have been incorporated, but they basically fulfill the same function, such as the Switch or the “Ternary conditional”.
Through analyzing their use, we know that they are a fundamental block for daily work, but overuse can cause some drawbacks. In other words, having too many conditions can be problematic for our code, and it can be even worse when the conditions are nested. This is when you need to be careful, because if you use them in excess, it can cause the code to:
- Be difficult to read and interpret.
- Have high complexity and become hard to maintain.
Therefore, when a conditional block becomes too large, it is a sign that you may be facing a ‘Code Smell’ that should be avoided if possible. Because of this, the key question is ‘what makes a block big?’. Well, on one hand, it’s obvious that the amount of code that it contains is a determining factor. To avoid having too much code crammed into one place, it can be divided into smaller divisions that perform specific tasks. And, on the other hand, we know the paths that the execution flow can take. These are, according to the conditional blocks that are part of the code, analyze which fragments of the code will be executed according to the needs of the parameters. This is called Cyclomatic Complexity (C.C.) analysis.
Both the “Code Smell” generated by a large number of lines in a method, and a high degree of Cyclomatic Complexity can give us an idea of the quality of the code we type. In short, if these two parameters are “high” it might be a good idea to think about some kind of refactorisation. In this article, we will firstly analyze what cyclomatic complexity is and how to calculate it. We will then discuss options and tools that can be considered when programming or refactoring existing code.
What is Cyclomatic Complexity?
The first to delve into this type of metric was Thomas McCabe, who was looking for a “mathematical” way to identify the difficulty of testing or maintaining a piece of code. Calculating this parameter can be useful to get an idea of the paths that the execution can follow, and the number of decisions that it can make. The number of possible paths will give us an idea of the complexity of the code. The greater this variable, the greater the complexity of the required maintenance. According to McCabe, this is intended to determine the scalability and confidence level of the system.
The decision flow of a method can be analyzed with a flow diagram composed of related nodes (representing a task) and vertices (showing the flow) that have an entry and exit point. The following graphic shows their basic structures:
According to McCabe, the Cyclomatic Complexity can be calculated as follows:
V(G) = (Aristas – Nodos) + 2
There are two other ways:
- The sum of the predicate nodes (nodes containing a condition) plus one:
V(G) = Nodos P + 1
- Another option is to see how many regions the graph has, considering outer space.
The following flow shows an example using a conditional and a While loop. To calculate the C.C. all three methods are used:
- V(G) = Aristas – Nodos + 2
= (8 -7) + 2 = 3
- V(G) = Predicate Nodes + 1
= 2 + 1 = 3
- Sum of areas:
1-Outside area.
2- Area under the node 2.
3- Area under the node 5.
Although the analysis of the code creating the graph gives us a good understanding of what is being done, having a visual element that can be attached to the documentation, as it takes time to deal with such complexities. For this reason, a practical way to obtain it without having to analyze and calculate each possibility is to use the VS. In VS2019 the tool can be used as seen in the following image.
Observations on Cyclomatic Complexity
As mentioned before, it is wise to try to keep the cyclomatic complexity as low as possible. For this reason, below are some ideas to avoid the conditional blocks that are one of the main sources of a high C.C.
Guard Clauses
A typical case that we come across on a daily basis is the need to control certain characteristics of objects or parameters, as can be seen in the example.
We can see that the C.C. for the code given is 3:
The code before verified that the Brand parameter is not null and that its property Name has a value. To refactor this verification, you can use a “Guard” tool such as Ensure That or Ardalis.GuardsClauses which can make it easy to replace the IF conditions through simple statements that do not add C.C. to our code. In the following images, you can witness the refactoring of the previous code using both libraries.
Table-Driven Design
We can study a case like the one seen in the following image where a method receives an object of exception type and we need to figure out what type it is in order to perform a special action in each case (a code fragment is shown).
Using the tool for calculating cyclomatic complexity from Visual Studio, you can see that the value reaches 11:
In order to avoid these high cyclomatic complexity levels, the Table-Driven Method can be used. How does it work?
The table management method is a very simple tool that consists, in a nutshell, of encapsulating the possible desired behaviors (or results) in a table and giving each one a form of access, which would work as a key. In this case, it would be the condition to please. By following this method, instead of having the chain of conditions, the table is directly asked to return what we need for the given case.
For the example discussed here, you can use a dictionary (key, value) whose key is a Type, and the value is a custom class that could be called, for example, ExceptionResult or whatever we want to return.
Subsequently, each of the exception types that the method can control and the possible result for each one are loaded as keys into the dictionary.
Once we have our dictionary, we could create a method that receives the exception, takes care of searching the dictionary, and returns the value found. In our example, we use the ConvertException method that was implemented as follows:
As you can see in the example, the type of the received exception is taken and used to get the ExceptionResult from the dictionary. Here, an extra mapping is done to rearm the Result.
Rule Pattern
A slightly more structured way of solving the previous problem would be the use of the Rule Pattern. This pattern allows you to encapsulate behaviors in classes, and at the same time, expose a method that makes it easy to select and execute the necessary rules. It is very similar to what was done in the previous point except that, instead of a dictionary, some structured classes are used.
In our case, 3 basic elements were used to implement the pattern: an interface (IRule) that establishes the structure of each rule, a class for each rule (in the example Rule1 and Rule2 that implement the interface), and finally a RuleEngine class which is the one that allows for the selection and use of each rule. The relationships between each element can be seen in the following diagram:
There are other ways to implement this pattern, but for the case analyzed in this text, the way presented here is sufficient. Once the idea has been raised, the implementation of the previous structure is presented below to solve the problem of handling exceptions. You must create as many “rules” as the domain requires. To make the example more understandable, in this case, only 3 were created.
Broadly speaking, it can be seen that the IExceptionRule interface requires each rule to implement two methods for each of the rules to be created:
- IsRequiredExceptionRule receives an Exception as a parameter and returns a boolean depending on whether the parameter is of the type of exception implemented in that rule.
- GetException which also receives an Exception as a parameter and returns a tuple with the code and the message expected for this type of exception.
Finally, the Engine class (in our example ExceptionRulesEngine) is in charge of selecting and/or using the rule, and providing the service to the module that needs it. This class contains the list of rules, which can be loaded into the constructor.
The Engine class contains a method that will be used externally (in the example of the ConvertException method). It receives an exception type, searches for it in the internal dictionary and returns the corresponding value. You can see its implementation within the image. It should be noted that a single IF is used, which decreases the C.C. compared to the large number of conditions that were originally available.
Final Thoughts on Cyclomatic Complexity
The cyclomatic complexity study is a tool that allows us to examine our code to determine how maintainable it is. It can also be used to determine if the written test cases are going through all the possible paths of our code. On the other hand, this metric can be taken into account as one of the parameters to analyze the quality of our code, but it is not the only metric that must be taken into account. There are more tools that add additional points of view, but this is a topic for another article.
If you have questions regarding the article, or require additional information on the topic, or want to connect with one of our software developers, please reach out!
—
Author
Nelson Ariel Garrido is a Senior Software Engineer with over 15 years of experience in development. He focues mainly in software architecture and backend using Microsoft technologies. He is finishing his PHD in Argentina where he is preparing his thesis on the use of AI in the development of assistive technology.