Refactoring Code — Taming the Spaghetti

Admir Mujkic
7 min readJun 7

--

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.

Spaghetti code

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();
}
}
}

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...
}
}

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 */ }

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;

To:

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

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
}

To:

private delegate bool NumberClassificationStrategy(int number);

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

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;
}

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}");
}

Final Touches

We can enhance readability by using string interpolation.

From:

Console.Write(i + " ");

To:

Console.Write($"{i} ");

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} ");
}
}
}

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! 👋

--

--

Admir Mujkic

Admir combined engineering expertise with business acumen to make a positive impact & share knowledge. Dedicated to educating the next generation of leaders.

Recommended from Medium

Lists

See more recommendations