Understanding the DRY Principle in C# Programming


The Don't Repeat Yourself (DRY) principle is a fundamental programming concept aimed at reducing redundancy in code. It emphasizes that every piece of knowledge must have a single, unambiguous representation within a system. When applied effectively, this principle not only improves code readability but also makes it easier to maintain and extend.


Let's have a look at some code extracted from our Math Game project.In this snippet we implement two separate methods for a subtraction and a multiplication game. Both methods contain nearly identical logic, leading to code duplication.

internal void SubtractionGame(string message)
		{
			Console.Clear();
			Console.WriteLine(message);
			var random = new Random();
			int firstNumber;
			int secondNumber;
			var score = 0;

			for (int i = 0; i < 5; i++)
			{
				firstNumber = random.Next(1, 9);
				secondNumber = random.Next(1, 9);
				Console.WriteLine($"{firstNumber} - {secondNumber}");
				var result = Console.ReadLine();
				result = Helpers.ValidateResult(result);

				if (int.Parse(result) == firstNumber - secondNumber)
				{
					Console.WriteLine("Your answer was correct! Type any key for the next question!");
					score++;
				}
				else
				{
					Console.WriteLine("Your answer wasn't correct! Type any key for the next question!");
				}

				if (i == 4) Console.WriteLine($"Game over! You score is {score}");
			}
			Helpers.AddToHistory(score, Models.Game.GameType.Subtraction);
		}

internal void MultiplicationGame(string message)
{
    Console.Clear();
    Console.WriteLine(message);
    var random = new Random();
    int firstNumber;
    int secondNumber;
    var score = 0;

    for (int i = 0; i < 5; i++)
    {
        firstNumber = random.Next(1, 9);
        secondNumber = random.Next(1, 9);
        Console.WriteLine($"{firstNumber} * {secondNumber}");
        var result = Console.ReadLine();
        result = Helpers.ValidateResult(result);
        if (int.Parse(result) == firstNumber * secondNumber)
        {
            Console.WriteLine("Your answer was correct! Type any key for the next question!");
            score++;
        }
        else
        {
            Console.WriteLine("Your answer wasn't correct! Type any key for the next question!");
        }

        if (i == 4) Console.WriteLine($"Game over! You score is {score}");
    }
    Helpers.AddToHistory(score, Models.Game.GameType.Multiplication);
}


The above implementation has an excessive amount of repetition. It's begging for some refactoring. Both methods share similar logic for clearing the console, displaying messages, and validating user input. This duplication not only makes the code harder to read but also complicates maintenance. If the logic for displaying questions or calculating scores needs to change, updates would have to be made in multiple places, increasing the likelihood of errors.


By refactoring the code using the DRY principle, we can create a single method that handles the common logic for all arithmetic operations. Here’s how the improved code looks:

public enum MathOperation
{
    Addition,
    Subtraction,
    Multiplication,
    Division
}

internal void MathGame(string message, MathOperation operation, int scoreIncrement, Func getNumbers, Models.Game.GameType gameType)
{
    Console.Clear();
    Console.WriteLine(message);
    var score = 0;

    for (int i = 0; i < 5; i++)
    {
        var numbers = getNumbers();
        var firstNumber = numbers[0];
        var secondNumber = numbers[1];

        string operatorSymbol = operation switch
        {
            MathOperation.Addition => "+",
            MathOperation.Subtraction => "-",
            MathOperation.Multiplication => "*",
            MathOperation.Division => "/",
            _ => throw new ArgumentException("Invalid operation")
        };

        Console.WriteLine($"{firstNumber} {operatorSymbol} {secondNumber}");
        var result = Console.ReadLine();
        result = Helpers.ValidateResult(result);

        int correctAnswer = operation switch
        {
            MathOperation.Addition => firstNumber + secondNumber,
            MathOperation.Subtraction => firstNumber - secondNumber,
            MathOperation.Multiplication => firstNumber * secondNumber,
            MathOperation.Division => firstNumber / secondNumber,
            _ => throw new ArgumentException("Invalid operation")
        };

        if (int.Parse(result) == correctAnswer)
        {
            Console.WriteLine("Your answer was correct! Type any key for the next question!");
            score += scoreIncrement;
        }
        else
        {
            Console.WriteLine("Your answer wasn't correct! Type any key for the next question!");
        }

        if (i == 4) Console.WriteLine($"Game over! Your score is {score}");
    }
    Helpers.AddToHistory(score, gameType);
}

Key Points to Consider


In this refactored version, the MathGame method encapsulates the shared logic for playing the game, including user prompts, input validation, and score tracking. Whatever code precedes the game simply calls MathGame with the appropriate parameters. This approach significantly reduces code duplication and enhances maintainability. If a change is needed, it can be made in one place, simplifying the update process and minimizing potential bugs.


With time and experience you'll develop an eye for opportunities to decrease redundancy in your code. The refactored example illustrates how a common logic can be abstracted into a single method, enabling various operations to share the same functionality while retaining clarity and flexibility. Embracing the DRY principle leads to better software design and ultimately improves the overall development experience.

Log in to mark this article as read and save your progress.
An unhandled error has occurred. Reload 🗙