Inappropriate coding practices - Cyclomatic complexity - Go

Inappropriate coding practices - Cyclomatic complexity - Go

Need

Improvement of coding practices to reduce cyclomatic complexity

Context

  • Usage of Go 1.16 for building efficient and scalable applications
  • Usage of gin-gonic/gin for building web applications and APIs in Go programming language

Description

Non compliant code

        package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/complex", func(c *gin.Context) {
		param1 := c.Query("param1")
		param2 := c.Query("param2")
		param3 := c.Query("param3")
		param4 := c.Query("param4")
		param5 := c.Query("param5")

		if param1 == "value1" {
			c.JSON(200, gin.H{
				"message": "value1",
			})
		} else if param2 == "value2" {
			c.JSON(200, gin.H{
				"message": "value2",
			})
		} else if param3 == "value3" {
			c.JSON(200, gin.H{
				"message": "value3",
			})
		} else if param4 == "value4" {
			c.JSON(200, gin.H{
				"message": "value4",
			})
		} else if param5 == "value5" {
			c.JSON(200, gin.H{
				"message": "value5",
			})
		} else {
			c.JSON(200, gin.H{
				"message": "default",
			})
		}
	})
	r.Run()
}
        
        

This is a simple example of a Go backend application using the Gin framework. It has a single endpoint /complex that accepts five different query parameters: param1, param2, param3, param4, param5. Depending on the value of these parameters, a different response is returned.

The cyclomatic complexity of this code is high due to the multiple conditional branches (if-else statements). Cyclomatic complexity is a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code.

High cyclomatic complexity increases the risk of bugs and makes the code harder to maintain and understand. It also increases the difficulty of testing since more test cases are required to achieve good code coverage. In this case, each new parameter added increases the complexity and the number of test cases needed.

Moreover, the use of multiple if-else statements can lead to nested code and increase the likelihood of errors. This can further lead to potential security vulnerabilities, making it difficult for developers to identify and resolve them.

In this case, each new query parameter increases the cyclomatic complexity, making the code more difficult to understand and maintain. This could potentially lead to the introduction of new security vulnerabilities and make their detection and resolution more difficult.

Steps

  • Refactor the code to reduce the cyclomatic complexity.
  • Use a switch statement instead of multiple if-else statements.
  • Consider using a map or dictionary to store the parameter-value pairs and their corresponding messages.
  • Implement a more structured and modular approach to handle different parameter-value combinations.
  • Consider using a strategy pattern or a rules engine to handle the logic based on the parameter values.

Compliant code

        package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/complex", handleRequest)
	r.Run()
}

func handleRequest(c *gin.Context) {
	params := map[string]string{
		"value1": "value1",
		"value2": "value2",
		"value3": "value3",
		"value4": "value4",
		"value5": "value5",
	}

	for key, value := range params {
		if c.Query(key) == value {
			c.JSON(200, gin.H{
				"message": value,
			})
			return
		}
	}

	c.JSON(200, gin.H{
		"message": "default",
	})
}
        
        

The original code had a high cyclomatic complexity due to the use of multiple if-else statements. This made the code difficult to understand, maintain, and potentially introduced new security vulnerabilities.

The refactored code reduces the cyclomatic complexity by using a map to store the parameter-value pairs and their corresponding messages. This approach simplifies the code and makes it easier to understand and maintain.

In the handleRequest function, we iterate over the map and check if the query parameter matches any of the keys in the map. If a match is found, we return a JSON response with the corresponding message. If no match is found after iterating over the entire map, we return a default message.

This approach not only reduces the cyclomatic complexity but also makes the code more modular and easier to extend. If we need to handle more parameter-value pairs in the future, we can simply add them to the map without modifying the core logic of the function.

References