DEV Community

Admir Mujkic
Admir Mujkic

Posted on

Refactoring Code — Taming the Spaghetti

Many of us have encountered situations where we find ourselves confronted with a complex and tangled piece of code, resembling a bowl of spaghetti. However, there is no need to fret! This blog post endeavors to explore the realm of refactoring, whereby we untangle our chaotic code and transfigure it into an elegant, modular, and comprehensible solution.

Before we start, what is spaghetti code?

Spaghetti code refers to source code that is messy and difficult to understand. It has a tangled structure, making it hard to maintain and likely to have errors.

Image description

Let’s explore an example of spaghetti code:

using System;
class Program {
    static void Main(string[] args) {
        var random = new Random();
        var flag = random.Next(1, 100) > 50;
        while (true) {
            Console.Write("Enter a number: ");
            var input = Console.ReadLine();
            if (input == "exit") break;
            if (!int.TryParse(input, out int number)) {
                Console.WriteLine("Input is not a number. Try again.");
                continue;
            }
            if (flag) {
                if (number % 2 == 0) {
                    Console.WriteLine("The number is even.");
                    for (var i = 0; i <= 10; i++) {
                        if (i == 5) break;
                        Console.Write(i + " ");
                    }
                } else {
                    Console.WriteLine("The number is odd.");
                    for (var i = 0; i <= 10; i++) {
                        if (i == 7) break;
                        Console.Write(i + " ");
                    }
                }
            } else {
                if (number % 3 == 0) {
                    Console.WriteLine("The number is even.");
                    for (var i = 0; i <= 10; i++) {
                        if (i == 5) break;
                        Console.Write(i + " ");
                    }
                } else {
                    Console.WriteLine("The number is odd.");
                    for (var i = 0; i <= 10; i++) {
                        if (i == 7) break;
                        Console.Write(i + " ");
                    }
                }
            }
            Console.WriteLine();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s approach the situation step by step and try to untangle the mess.

Identify the Problem

One of the initial issues we face is the lack of meaningful variable names. Meaningful and descriptive names greatly enhance code readability. To untangle the code, our first step is to rename variables so that they accurately represent their purpose, making the code self-explanatory. This best practice improves our own understanding and helps future developers working on the code.

The code’s lack of modularity is evident in the oversized Main function that performs multiple tasks. To tackle this problem, we will break down the Main function and identify separate functionalities. These will be extracted into their own functions or classes. This approach allows us to group related logic together, promote code reusability, and enhance the overall structure of the codebase.

Another important aspect that needs improvement is the code’s limited error handling. Effective error handling is crucial for identifying and gracefully recovering from unexpected situations. To address this, we will examine potential exceptions that may occur during the code’s execution. We will then implement appropriate error handling mechanisms, such as try-catch blocks, to ensure the code handles exceptions smoothly and provides helpful error messages.

The code contains numerous complex if-else conditions, making it difficult to read and understand. To simplify this, we will utilize techniques like switch statements, polymorphism, or design patterns such as the strategy pattern. These approaches help streamline conditional logic and reduce its complexity. By doing so, we improve the code’s readability, maintainability, and make it easier to modify in the future.

Break it Down

To begin untangling the code, our first step is to split it into smaller, manageable chunks. This allows us to focus on specific functionalities and improve modularity. By breaking down the code into smaller parts, we can isolate and understand individual components more effectively, making it easier to maintain and enhance the codebase.

if (flag) {
    if (number % 2 == 0) {
        Console.WriteLine("The number is even.");
        for (var i = 0; i <= 10; i++) {
            if (i == 5) break;
            Console.Write(i + " ");
        }
    } else {
        // Other similar codes...
    }
}
Enter fullscreen mode Exit fullscreen mode

To improve the modularity of the code, we can start by separating the logic responsible for determining the number type and the logic for printing numbers into separate functions or classes.

  1. Create a function or class called DetermineNumberType that takes a number as input and handles the logic for determining the number type. This function/class should analyze the number and return its type, such as “even,” “odd,” “prime,” or any other relevant categories.
  2. Next, create a separate function or class named PrintNumbersUpTo that handles the logic for printing numbers up to a given limit. This function/class should take the limit as an input and iterate through the numbers, calling the DetermineNumberType function/class for each number and printing the results.
static string DetermineNumberType(int number) { /* logic here */ }
static void PrintNumbersUpTo(int terminationNumber) { /* logic here */ }
Enter fullscreen mode Exit fullscreen mode

Use Descriptive Names

To enhance code readability and maintainability, it’s essential to give meaningful names to variables and methods. Let’s apply this practice to the code by assigning appropriate names to the relevant elements.

From:

var random = new Random();
var flag = random.Next(1, 100) > 50;
Enter fullscreen mode Exit fullscreen mode

To:

private static readonly Random randomNumberGenerator = new Random();
bool isStrategyForEvenNumbers = randomNumberGenerator.Next(1, 100) > 50;
Enter fullscreen mode Exit fullscreen mode

Use Design Patterns and Principles

To enhance code readability and reduce the complexity of conditionals, we can employ the Strategy Pattern. The Strategy Pattern allows us to encapsulate different algorithms or strategies and dynamically select one at runtime. Here’s how we can apply the Strategy Pattern to replace complex conditionals:

From:

if (flag) {
    if (number % 2 == 0) {
        // some code
    } else {
        // some code
    }
} else {
    // more code
}
Enter fullscreen mode Exit fullscreen mode

To:

private delegate bool NumberClassificationStrategy(int number);

private static readonly Dictionary<bool, NumberClassificationStrategy> numberTypeDeterminationStrategies = 
new Dictionary<bool, NumberClassificationStrategy>
{
    { true, IsEven },
    { false, IsDivisibleByThree }
}
Enter fullscreen mode Exit fullscreen mode

By utilizing the Strategy Pattern, we achieve a more modular and maintainable code structure. It simplifies the complex conditional logic, improves code readability, and makes it easier to extend or modify the behavior in the future.

Improve Error Handling

The original code had minimal error handling. Let’s improve that.

From:

if (!int.TryParse(input, out int number)) {
    Console.WriteLine("Input is not a number. Try again.");
    continue;
}
Enter fullscreen mode Exit fullscreen mode

To:

try
{
    if (!int.TryParse(input, out int number)) {
        Console.WriteLine("Input is not a number. Try again.");
        continue;
    }
}
catch (Exception ex)
{
    Console.WriteLine($"An error occurred: {ex.Message}");
}

Enter fullscreen mode Exit fullscreen mode

Final Touches
We can enhance readability by using string interpolation.

From:

Console.Write(i + " ");
Enter fullscreen mode Exit fullscreen mode

To:

Console.Write($"{i} ");
Enter fullscreen mode Exit fullscreen mode

And there you have it! Our final, refactored code is clear, modular, and “easy” to understand.

using System;
using System.Collections.Generic;

class Program
{
    private const int MaxPrintLimit = 10;
    private const int ExitCommandCode = -1;
    private const int RandomThresholdForNumberType = 50;
    private const string EvenNumberIndicator = "even";
    private const string OddNumberIndicator = "odd";

    private static readonly Random randomNumberGenerator = new Random();

    private delegate bool NumberClassificationStrategy(int number);

    private static readonly Dictionary<bool, NumberClassificationStrategy> numberTypeDeterminationStrategies = 
    new Dictionary<bool, NumberClassificationStrategy>
    {
        { true, IsEven },
        { false, IsDivisibleByThree }
    };

    static void Main()
    {
        try
        {
            while (true)
            {
                int userInput = RetrieveUserInput();
                if (userInput == ExitCommandCode) break;

                string determinedNumberType = DetermineNumberType(userInput);
                Console.WriteLine($"The number is {determinedNumberType}.");
                PrintNumberSequence(determinedNumberType);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An error occurred: {ex.Message}");
        }
    }

    static int RetrieveUserInput()
    {
        while (true)
        {
            try
            {
                Console.Write("Enter a number: ");
                string input = Console.ReadLine();

                if (input == "exit") return ExitCommandCode;

                if (int.TryParse(input, out int parsedNumber)) return parsedNumber;

                throw new FormatException("Input is not a number. Try again.");
            }
            catch (FormatException fe)
            {
                Console.WriteLine(fe.Message);
            }
        }
    }

    static string DetermineNumberType(int number)
    {
        bool randomFlag = randomNumberGenerator.Next(1, 100) > RandomThresholdForNumberType;

        if (numberTypeDeterminationStrategies.TryGetValue(randomFlag, out NumberClassificationStrategy numberClassificationMethod))
        {
            return numberClassificationMethod(number) ? EvenNumberIndicator : OddNumberIndicator;
        }
        else
        {
            throw new KeyNotFoundException("The strategy for number type determination could not be found.");
        }
    }

    static bool IsEven(int number)
    {
        return number % 2 == 0;
    }

    static bool IsDivisibleByThree(int number)
    {
        return number % 3 == 0;
    }

    static void PrintNumberSequence(string numberType)
    {
        int terminationNumber = numberType == EvenNumberIndicator ? 5 : 7;

        PrintNumbersUpTo(terminationNumber);
        Console.WriteLine();
    }

    static void PrintNumbersUpTo(int terminationNumber)
    {
        for (int i = 0; i <= MaxPrintLimit; i++)
        {
            if (i == terminationNumber)
            {
                return;
            }
            Console.Write($"{i} ");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Cyclomatic Complexity

Cyclomatic Complexity (CC) is a metric that measures the complexity of a program. It quantifies the number of independent paths through the program’s source code. For simple if-else constructs, you can estimate the cyclomatic complexity by counting the decision points (such as if, while, for statements) and adding one.

In the old code:

5 if statements
1 while loop
2 for loops
Adding them up and adding one, the cyclomatic complexity is 9.

In the new code:

2 if statements
1 while loop
No for loops (encapsulated in a method)
The cyclomatic complexity of the new code is 4.

Reducing cyclomatic complexity simplifies the code, reduces the likelihood of errors, and improves maintainability.

Finally

In conclusion, through refactoring, we’ve made our code more maintainable, easier to understand, and reduced the cyclomatic complexity from 9 to 4. It’s a solid win for any developer.

Remember, great code is not about how complex you can make it, but how simple you can make it. As Albert Einstein said, “Everything should be made as simple as possible, but no simpler”. Happy coding!


P.S. If you believe I can help with something, don’t hesitate to contact me, and I will reach you as soon as possible. admir.m@penzle.com

Cheers! 👋


References:

Clean Code in C#: Refactor your legacy C# code base and improve application performance by applying…
Clean Code in C#: Refactor your legacy C# code base and improve application performance by applying best practices…
www.amazon.com

Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series…
Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler)) [Fowler…
www.amazon.com

Top comments (1)

Collapse
 
pcmagas profile image
Dimitrios Desyllas

The 99% of the cases the Identify the Problem is the biggest hurdle