© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
V. SarcarSimple and Efficient Programming with C# https://doi.org/10.1007/978-1-4842-8737-8_5

5. Use the DRY Principle

Vaskaran Sarcar1  
(1)
Kolkata, West Bengal, India
 
This chapter discusses the don’t repeat yourself (DRY) principle. It is another important principle that a professional coder follows when writing a piece of code in an application. The SRP and OCP principles that you learned about in Chapter 4 are also related to the DRY principle. Andy Hunt and Dave Thomas first wrote about this principle in their book The Pragmatic Programmer. The DRY principle is stated as follows:
  • Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

This may seem complicated when you read it for the first time. The goal of this chapter is to help you understand this principle with a case study.

Reasons for DRY

Code duplication can cause an application to fail. Programmers often call any duplication an evil in software. So the question is, why do we see any duplicated code at all? There are a variety of reasons. Here are some examples:
  • A programmer cannot resist doing a simple copy/paste, which appears as the shortest path to success.

  • The project deadline is approaching. The developer assumes that a certain number of duplicates are OK at this moment. The developer plans to remove these duplicates in the next release but forgets to do so.

  • Duplicates are seen in code comments too. Say a developer knows the code very well and does not need the documentation to understand the logic of the code. A new requirement forced the developer to update a portion of the code. So, the developer starts working with an existing code and its associated comments. Once the update is done, due to various reasons, the developer forgets to update the associated comments.

  • A tester may need to pass the same input to verify various methods in a test suite.

  • Sometimes duplicates are hard to avoid. Project standards may require a developer to put duplicate information in the code.

  • Suppose your software targets multiple platforms that use different programming languages and/or development environments. In this case, you may need to duplicate shared information (such as methods).

  • In addition, a programming language can have a structure that duplicates some information.

In computer science, developers follow many principles to avoid code duplications. For example, database normalization techniques try to eliminate duplicate data. In Chapter 2, you saw that you can put a common method in an abstract base class to avoid duplicating the method in the derived classes.

I must say that finding duplicate code is not always easy. For example, consider the following code segment with two methods.

Code segment 1:
public void DisplayCost()
{
 Console.WriteLine("Game name: SuperGame");
 Console.WriteLine("Version:1.0");
 Console.WriteLine("Actual cost is: $1000");
}
public void DisplayCostAfterDiscount()
{
 Console.WriteLine("Game name: SuperGame");
 Console.WriteLine("Version:1.0");
 Console.WriteLine("The discounted price for festive
  season is:$800");
}

You can easily see that the initial two lines are common to both of these methods. But the same kind of detection is not straightforward if the duplications are intermixed with other code/comments. For example, consider code segment 2.

Code segment 2:
public void DisplayCost()
{
 Console.WriteLine("AbcCompany SuperGame's price details:");
 Console.WriteLine("Version:1.0 cost is: $1000");
}
public void DisplayCostAfterDiscount()
{
 Console.WriteLine("AbcCompany offers festive season discount.");
 Console.WriteLine("Discounted price detail:");
 Console.WriteLine("Game: SuperGame. Version: 1.0.
  Discounted price:$800");
}

On careful observation, you can find that in both code segments, the company name, game name, and version detail of the software are repeated. In the first code segment, it is easy to find the duplicate code, but in the second code segment, you need to read the code carefully.

These code segments contain only two methods. In a real-world application, you would see a lot of methods, and not all the methods are present in the same file. So, if you spread duplicate information across the files, a simple update can cause the software to show inconsistent behavior.

During an update operation, if you have n number of duplicates, you need n-fold modification, and you cannot miss any of them. This is why you need to be careful about them. Violating the DRY principle causes a WET solution, which commonly stands for “write every time,” “write everything twice,” “we enjoy typing,” or “waste everyone’s time.” Like the previous chapters, I start with a program that seems fine at the beginning. I then analyze this program and make it better by eliminating redundant code. You can follow the same approach when you encounter a similar situation.

Initial Program

Here is a simplistic example for you. The following are the key assumptions in this program:
  • There is game software called SuperGame. You create a class to represent this game.

  • The AboutGame() method provides some useful information about this software. For example, it says that the minimum age for using this software is 10. It also shows the current version of the game.

  • The DisplayCost() methods specifies the price of the latest version of this software.

  • A buyer can get up to a 20 percent discount. The DisplayCostAfterDiscount() method shows the discounted price of the latest software.

Demonstration 1

Assume that someone has written the following program. It compiles and runs successfully. Let’s see the output and then read the “Analysis” section.
Console.WriteLine("***A demo without the DRY principle. ***");
SuperGame superGame = new ();
superGame.AboutGame();
superGame.DisplayCost();
superGame.DisplayCostAfterDiscount();
class SuperGame
{
    public void AboutGame()
    {
       Console.WriteLine("Game name: SuperGame");
       Console.WriteLine("Minimum age: 10 years and above.");
       Console.WriteLine("Current version: 1.0.");
       Console.WriteLine("It is the AbcCompany product.");
    }
    public void DisplayCost()
    {
       Console.WriteLine(" AbcCompany SuperGame's price details:");
       Console.WriteLine("Version:1.0 Cost:$1000");
    }
    public void DisplayCostAfterDiscount()
    {
       Console.WriteLine(" AbcCompany offers a festive season discount.");
       Console.WriteLine("Discounted price details:");
       Console.WriteLine("Game: SuperGame. Version:
         1.0 Discounted price: $800");
    }
}

Output

Here is the output:
***A demo without the DRY principle. ***
Game name: SuperGame
Minimum age: 10 years and above.
Current version: 1.0.
It is the AbcCompany product.
AbcCompany SuperGame’s price details:
Version:1.0
Cost: $1000
AbcCompany offers a festive season discount.
Discounted price details:
Game: SuperGame.
Version: 1.0
Discounted price: $800

Analysis

Can you see the problems with this program? How many times are you seeing the company name AbcCompany and the version detail in this program? I know that it’s a simple program, but consider the more complex case I mentioned earlier. These methods can reside in different modules, and if you needed to update the company information or the version details, you would need to figure out all of those places before you provide the update. This is why the DRY principle needs to be applied.

This program suffers from the use of hard-coded strings. The solution to this problem is straightforward. You can use a single place to contain these strings that appear in multiple parts of your program. Then you share this code segment with other parts of the program. As a result, when you update a string in the shared location, the change reflects properly in every place.

The basic idea is that if you see common code in multiple locations, you separate the common parts from the remaining parts, put them in a single location, and call this common code from other parts of the program. In this way, you avoid the copy/paste technique, which may seem appealing at the beginning but creates problems as time goes on.

Note There is another principle called the once and only once (OAOO) principle, which is similar to the DRY principle. The page at https://wiki.c2.com/?OnceAndOnlyOnce states that every declaration of behavior should appear once and only once. So, the OAOO principle is helpful in the context of functional behavior, and you can apply this while refactoring your code. You can think of it as a subset of DRY because the DRY principle is not limited to code and design; you can apply it to other project artifacts as well. For example, you can automate the code integration process instead of repeating it. In simple words, the core ideas of DRY and OAOO are the same.

Can you make demonstration 1 better? Let’s look at the following program.

Better Program

This program uses a constructor to initialize the values. You can use these values in the instance methods of the class.

POINTS TO NOTE
You’ll see that I have used the raw string literals in the upcoming program. You know that this is a C# 11 preview feature. So, instead of using the following line:
  Console.WriteLine($"Version:{version} " +
                    $"Cost: {actualCost}");
I could have used the following to produce the same output:
  Console.WriteLine($"""
                   Version:{version}
                   Cost: {actualCost}
                   """);

Demonstration 2

Here is an improved version:
Console.WriteLine("***Demonstration 2: An improved version***");
SuperGame superGame = new ();
superGame.AboutGame();
superGame.DisplayCost();
superGame.DisplayCostAfterDiscount();
class SuperGame
{
    readonly string companyName;
    readonly string gameName;
    readonly double minimumAge;
    readonly string version;
    readonly double actualCost;
    readonly double discountedCost;
    public SuperGame()
    {
        companyName = "AbcCompany";
        gameName = "SuperGame";
        version = "1.0";
        minimumAge = 10;
        actualCost = 1000;
        discountedCost = 800;
    }
    public void AboutGame()
    {
        Console.WriteLine($"Game name: {gameName}");
        Console.WriteLine($"Minimum age: {minimumAge}
          years and above.");
        Console.WriteLine($"Current version:
         {version}.");
        Console.WriteLine($"It is a {companyName}
          product.");
    }
    public void DisplayCost()
    {
        Console.WriteLine($" {companyName}
          SuperGame’s price details:");
        Console.WriteLine($"""
                          Version:{version}
                          Cost: {actualCost}
                          """);
    }
    public void DisplayCostAfterDiscount()
    {
        Console.WriteLine($" {companyName} offers a
          festive season discount.");
        Console.WriteLine("Discounted price detail:");
        Console.WriteLine($"""
               Game: {gameName}.
               Version: {version}.
               Discounted price: {discountedCost}
               """);
    }
}

Output

Here is the output of this program:
***Demonstration 2: An improved version***
Game name: SuperGame
Minimum age: 10 years and above.
Current version: 1.0.
It is the AbcCompany product.
AbcCompany SuperGame’s price details:
Version:1.0
Cost: 1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame.
Version: 1.0.
Discounted price:800

You can see that this program produces the same output except for the first line, which prints that it is an improved version.

Analysis

You can see that the issues with the hard-coded strings are taken care of in demonstration 2. Still, there is some repetition in this example. Notice that the company name is shown in AboutGame(), DisplayCost(), and DisplayCostAfterDiscount(). This was OK for me because I wanted to display the company name in any of the methods that a client may use.

But you can improve this program. The initial version of the software and the company name may not change for a different game (which is made by the same company), but the name of the game and price details are likely to change. So, let’s improve the program logic and work further in these areas. In addition, if you understand the SOLID principles from Chapter 4, you know that this program does not follow the SRP.

In short, you may need to update this program in the future due to various reasons. Some of them are as follows:
  • The cost of the software can be changed.

  • The discounted price can be changed.

  • The version detail can be changed.

  • The name of the game can be changed.

  • Also, consider the case when the company name itself can be changed.

So, I move the company name, game name, version, and age requirement into a new class called GameInfo. The actual price and the discounted price are moved into a different class called GamePrice. In addition, this time I use properties so that you can apply some changes to the initial values at a later stage.

In this upcoming program, when you instantiate a GameInfo instance, you supply the name of the game, but before that, you initialize a GameInfo instance and a GamePrice instance. This activity helps you to instantiate a game instance with the default information stored in GameInfo and GamePrice. As I said before, you can change these values using various properties of these classes.

Further Improvement

Now let’s go through the proposed improvement. You can follow a similar structure to adapt to a new requirement with a small change.

Demonstration 3

Here is an improved version of demonstration 2 (I have kept some comments in to help you understand the concepts):
Console.WriteLine("*** Another improved version
  following the DRY principle. ***");
// Initial setup
GameInfo gameInfo = new("SuperGame");
GamePrice gamePrice = new();
// Create the game instance with the default setup
Game game = new(gameInfo, gamePrice);
// Display the default game detail.
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.WriteLine("------------");
Console.WriteLine("Changing the game version and price now.");
// Changing some of the game info
gameInfo.Version = "2.0";
gameInfo.MinimumAge = 9.5;
// Changing the game cost
gamePrice.Cost = 1500;
gamePrice.DiscountedCost = 1200;
// Updating the game instance
game = new Game(gameInfo, gamePrice);
// Display the latest detail
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
class GameInfo
{
    public string CompanyName { get; set; }
    public string GameName { get; set; }
    public string Version { get; set; }
    public double MinimumAge { get; set; }
    public GameInfo(string gameName)
    {
        CompanyName = "AbcCompany";
        GameName = gameName;
        Version = "1.0";
        MinimumAge = 10.5;
    }
}
class GamePrice
{
    public double Cost { get; set; }
    public double DiscountedCost { get; set; }
    public GamePrice()
    {
        Cost = 1000;
        DiscountedCost = 800;
    }
}
class Game
{
    readonly string companyName;
    readonly string gameName;
    readonly double minimumAge;
    readonly string version;
    readonly double actualCost;
    readonly double discountedCost;
    public Game(GameInfo gameInfo,
                GamePrice gamePrice)
    {
        companyName = gameInfo.CompanyName;
        gameName = gameInfo.GameName;
        version = gameInfo.Version;
        minimumAge = gameInfo.MinimumAge;
        actualCost = gamePrice.Cost;
        discountedCost = gamePrice.DiscountedCost;
    }
    public void AboutGame()
    {
        Console.WriteLine($"Game name: {gameName}");
        Console.WriteLine($"Minimum age: {minimumAge}
          years and above.");
        Console.WriteLine($"Current version:
         {version}.");
        Console.WriteLine($"It is the {companyName}
          product.");
    }
    public void DisplayCost()
    {
        Console.WriteLine($" {companyName}
         {gameName}'s price details:");
        Console.WriteLine($"""
                          Version: {version}
                          Cost: {actualCost}
                          """);
    }
    public void DisplayCostAfterDiscount()
    {
        Console.WriteLine($" {companyName} offers a
         festive season discount.");
        Console.WriteLine("Discounted price detail:");
        Console.WriteLine($"""
               Game: {gameName}.
               Version: {version}.
               Discounted price: {discountedCost}
               """);
    }
}

Output

Here is the new output that reflects the changes in various fields:
*** Another improved version following the DRY principle. ***
Game name: SuperGame
Minimum age: 10.5 years and above.
Current version: 1.0.
It is the AbcCompany product.
AbcCompany SuperGame's price details:
Version:1.0
Cost: 1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame.
Version: 1.0.
Discounted price:800
------------
Changing the game version and price now.
Game name: SuperGame
Minimum age: 9.5 years and above.
Current version: 2.0.
It is the AbcCompany product.
AbcCompany SuperGame's price details:
Version:2.0
Cost: 1500
AbcCompany offers a festive season discount.
Discounted price detail:
Game: SuperGame.
Version: 2.0.
Discounted price:1200

Is it the end of the improvements? You know the answer to that. There is no end to improvement; you can always improve your code. You know that a company does not finish with making a single game. A company can create multiple games, but they can share a common format to display information about the games. So, if tomorrow the company wants you to make a new game, say NewPokemonKid, how should you proceed? Will you copy/paste the existing code and start editing? You know that this process is not recommended at all.

You can make this program better if you move the Game, GameInfo, and GamePrice classes to a shared library and use them accordingly. When you do this, you again follow the DRY principle because you do not copy/paste the existing code to make a new game or to enhance a requirement. Instead, you reuse an existing solution that works fine, and using this you indirectly save your test time.

So, I create a class library project called BasicGameInfo and create a namespace, and then I move these classes into a common file, CommonLibrary.cs (I renamed it from class1.cs). I make these classes public so that I can access them from a different file. Creating a namespace was optional for me, but I followed Visual Studio’s suggestion here. To make the code more professional, this time I add a few descriptions of these classes too. You can see them in the upcoming demonstration.

For your immediate reference, see the Solution Explorer view in Figure 5-1 where I use a BasicGameInfo project reference in the Demo4_DRYUsingDll project.
Figure 5-1

Demo4_DryDemoUsingDll is using the BasicGameInfo project reference

After I created the project Demo4_DRYUsingDll, I added the BasicGameInfo reference. Figure 5-2 shows you a sample snapshot when I right-click the project dependencies, add the reference, and about to click the OK button.
Figure 5-2

Adding BasicGameInfo reference to a C# project file

Now you can add using BasicGameInfo; at the beginning of your new file. This helps you to type less code. For example, you can directly use Game instead of BasicGameInfo.Game. The same comment applies to GameInfo and GamePrice.

Demonstration 4

To show you a sample demo, this time I change a few parameters, like the name of the game, version, price detail, etc. Here I put all the pieces together for your easy reference. You’ll notice that this updated client code is similar to the client code that you saw in the previous demonstration. Here is the complete program:
// The content of CommonLibrary.cs
namespace BasicGameInfo
{
    /// <summary>
    /// Provides the company name, game name,
    /// version and the age criteria.
    /// </summary>
    public class GameInfo
    {
        public string CompanyName { get; set; }
        public string GameName { get; set; }
        public string Version { get; set; }
        public double MinimumAge { get; set; }
        public GameInfo(string gameName)
        {
            CompanyName = "AbcCompany";
            GameName = gameName;
            Version = "1.0";
            MinimumAge = 10.5;
        }
    }
    /// <summary>
    /// Shows the actual price and the
    /// discounted price of the game.
    /// </summary>
    public class GamePrice
    {
        public double Cost { get; set; }
        public double DiscountedCost { get; set; }
        public GamePrice()
        {
            Cost = 1000;
            DiscountedCost = 800;
        }
    }
    /// <summary>
    /// Provides different methods to retrieve
    /// the game information.
    /// </summary>
    public class Game
    {
        readonly string companyName;
        readonly string gameName;
        readonly double minimumAge;
        readonly string version;
        readonly double actualCost;
        readonly double discountedCost;
        public Game(GameInfo gameInfo,
          GamePrice gamePrice)
        {
            companyName = gameInfo.CompanyName;
            gameName = gameInfo.GameName;
            version = gameInfo.Version;
            minimumAge = gameInfo.MinimumAge;
            actualCost = gamePrice.Cost;
            discountedCost = gamePrice.DiscountedCost;
        }
        public void AboutGame()
        {
            Console.WriteLine($"Game name:
              {gameName}");
            Console.WriteLine($"Minimum age:
              {minimumAge} years and above.");
            Console.WriteLine($"Current version:
              {version}.");
            Console.WriteLine($"It is a {companyName}
             product.");
        }
        public void DisplayCost()
        {
            Console.WriteLine($" {companyName}
              {gameName}'s price details:");
            Console.WriteLine($"""
              Version:{version}
              Cost: {actualCost}
              """);
        }
        public void DisplayCostAfterDiscount()
        {
            Console.WriteLine($" {companyName} offers
              a festive season discount.");
            Console.WriteLine("Discounted price
              detail:");
            Console.WriteLine($"""
              Game: {gameName}.
              Version: {version}.
              Discounted price:{discountedCost}
              """);
        }
    }
}
// The content of the new client code
using BasicGameInfo;
Console.WriteLine("*** Applying the DRY principle
  using a DLL. ***");
// Initial setup
GameInfo gameInfo = new ("NewPokemonKid");
GamePrice gamePrice = new ();
// Create the game instance with a
// default setup
Game game = new(gameInfo, gamePrice);
// Display the default game detail.
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();
Console.WriteLine("------------");
Console.WriteLine("Changing the game version and price
 now.");
// Changing some of the game info
gameInfo.Version = "2.1";
gameInfo.MinimumAge = 12.5;
// Changing the game cost
gamePrice.Cost = 3500;
gamePrice.DiscountedCost = 2000;
// Updating the game instance
game = new Game(gameInfo, gamePrice);
// Display the latest detail
game.AboutGame();
game.DisplayCost();
game.DisplayCostAfterDiscount();

Output

When you run this program, you get the following output:
*** Applying the DRY principle using a DLL. ***
Game name: NewPokemonKid
Minimum age: 10.5 years and above.
Current version: 1.0.
It is a AbcCompany product.
AbcCompany NewPokemonKid's price details:
Version:1.0
Cost: 1000
AbcCompany offers a festive season discount.
Discounted price detail:
Game: NewPokemonKid.
Version: 1.0.
Discounted price:800
------------
Changing the game version and price now.
Game name: NewPokemonKid
Minimum age: 12.5 years and above.
Current version: 2.1.
It is the AbcCompany product.
AbcCompany NewPokemonKid's price details:
Version:2.1
Cost: 3500
AbcCompany offers a festive season discount.
Discounted price detail:
Game: NewPokemonKid.
Version: 2.1.
Discounted price:2000

Once again we get the desired result by following the DRY principle and reusing existing code.

I know what you are thinking. You see a tight coupling between Game and GameInfo/GamePrice. Yes, that is true. How can you remove this coupling? Since you learned about the DIP in the previous chapter, this should not be a problem for you. I will leave this as an exercise for you.

Summary

Code duplication can cause serious problems for software. Expert programmers often treat these duplications as evils in the software. Why do we see duplicate code? There are a variety of reasons; some of them are attractive, and some of them are hard to avoid. But by removing the redundant code, you make better software that is easier to maintain.

This chapter showed you how to apply the DRY principle. You saw an initial version of a program that was improved multiple times to make it better. Finally, you moved the common code to a shared library.

This principle applies not only to the actual code but also to code comments and test cases. For example, you can make a common input file to test various methods instead of passing the same input repeatedly in every method. When you consider using code comments, try to keep the low-level knowledge in the code and use the comments for high-level explanations. Otherwise, for each update, you need to change both the code and the comments.

You’ll probably agree with me that repeated code causes higher development costs and more maintenance problems. The DRY principle discourages those activities. Since it promotes reusability, it accelerates the overall development process in the long run. In short, this principle helps you write cleaner and better code. So, by using this principle, you make better software.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.134.114