6 min read

In today’s tutorial, we’ll learn how to apply the Single Responsibility principle from the SOLID principles, to .NET Core Applications. This brings us to another interesting concept in OOP called SOLID design principles. These design principles are applied to any OOP design and are intended to make software easier to understand, more flexible, and easily maintainable.

This article is an extract from the book C# 7 and .NET Core Blueprints, authored by Dirk Strauss and Jas Rademeyer. The book is a step-by-step guide that will teach you the essential .NET Core and C# concepts with the help of real-world projects.

The term SOLID is a mnemonic for:

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

In this article, we will take a look at the first of the principles—the single responsibility principle. Let’s look at the single responsibility principle now.

Single responsibility principle

Simply put, a module or class should have the following characteristics only:

  • It should do one single thing and only have a single reason to change
  • It should do its one single thing well
  • The functionality provided needs to be entirely encapsulated by that class or module

What is meant when saying that a module must be responsible for a single thing? The Google definition of a module is:

“Each of a set of standardized parts or independent units that can be used to construct a more complex structure, such as an item of furniture or a building.”

From this, we can understand that a module is a simple building block. It can be used or reused to create something bigger and more complex when used with other modules. In C# therefore, the module does closely resemble a class, but I will go so far as to say that a module can also be extended to be a method.

The function that the class or module performs can only be one thing. That is to say that it has a narrow responsibility. It is not concerned with anything else other than doing that one thing it was designed to do.

If we had to apply the single responsibility principle to a person, then that person would be only a software developer, for example. But what if a software developer also was a doctor and a mechanic and a school teacher? Would that person be effective in any of those roles? That would contravene the single responsibility principle. The same is true for code.

Having a look at our AllRounder and Batsman classes, you will notice that in AllRounder, we have the following code:

private double CalculateStrikeRate(StrikeRate strikeRateType) 
{ 
    switch (strikeRateType) 
    { 
        case StrikeRate.Bowling: 
            return (BowlerBallsBowled / BowlerWickets); 
        case StrikeRate.Batting: 
            return (BatsmanRuns * 100) / BatsmanBallsFaced; 
        default: 
            throw new Exception("Invalid enum"); 
    } 
} 
 
public override int CalculatePlayerRank() 
{ 
    return 0; 
} 

In Batsman, we have the following code:

public double BatsmanBattingStrikeRate => (BatsmanRuns * 100) / BatsmanBallsFaced;  
 
public override int CalculatePlayerRank() 
{ 
    return 0; 
}

Using what we have learned about the single responsibility principle, we notice that there is an issue here. To illustrate the problem, let’s compare the code side by side:

single responsibility principle

We are essentially repeating code in the Batsman and AllRounder classes. This doesn’t really bode well for single responsibility, does it? I mean, the one principle is that a class must only have a single function to perform. At the moment, both the Batsman and AllRounder classes are taking care of calculating strike rates. They also both take care of calculating the player rank. They even both have exactly the same code for calculating the strike rate of a batsman!

The problem comes in when the strike rate calculation changes (not that it easily would, but let’s assume it does). We now know that we have to change the calculation in both places. As soon as the developer changes one calculation and not the other, a bug is introduced into our application.

Let’s simplify our classes. In the BaseClasses folder, create a new abstract class called Statistics. The code should look as follows:

namespace cricketScoreTrack.BaseClasses 
{ 
    public abstract class Statistics 
    { 
        public abstract double CalculateStrikeRate(Player player); 
        public abstract int CalculatePlayerRank(Player player); 
    } 
}

In the Classes folder, create a new derived class called PlayerStatistics (that is to say it inherits from the Statistics abstract class). The code should look as follows:

using cricketScoreTrack.BaseClasses; 
using System; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class PlayerStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 
         
        public override double CalculateStrikeRate(Player player) 
        {             
            switch (player) 
            { 
                case AllRounder allrounder: 
                    return (allrounder.BowlerBallsBowled / 
                     allrounder.BowlerWickets); 
                     
                case Batsman batsman: 
                    return (batsman.BatsmanRuns * 100) / 
                     batsman.BatsmanBallsFaced; 
                     
                default: 
                    throw new ArgumentException("Incorrect argument 
                     supplied"); 
            } 
        } 
    } 
}

You will see that the PlayerStatistics class is now solely responsible for calculating player statistics for the player’s rank and the player’s strike rate.

You will see that I have not included much of an implementation for calculating the player’s rank. I briefly commented the code on GitHub for this method on how a player’s rank is determined. It is quite a complicated calculation and differs for batsmen and bowlers. I have therefore omitted it for the purposes of this chapter on OOP.

Your Solution should now look as follows:

Solution explorer

Swing back over to your Player abstract class and remove abstract public int CalculatePlayerRank(); from the class. In the IBowler interface, remove the double BowlerStrikeRate { get; } property. In the IBatter interface, remove the double BatsmanBattingStrikeRate { get; } property.

In the Batsman class, remove public double BatsmanBattingStrikeRate and public override int CalculatePlayerRank() from the class. The code in the Batsman class will now look as follows:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class Batsman : Player, IBatter 
    { 
         
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 
 
        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 
        #endregion 
    } 
}

Looking at the AllRounder class, remove the public enum StrikeRate { Bowling = 0, Batting = 1 } enum as well as the public double BatsmanBattingStrikeRate and public double BowlerStrikeRate properties.

Lastly, remove the private double CalculateStrikeRate(StrikeRate strikeRateType) and public override int CalculatePlayerRank() methods. The code for the AllRounder class now looks as follows:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 
using System; 
 
namespace cricketScoreTrack.Classes 
{ 
    public class AllRounder : Player, IBatter, IBowler 
    { 
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 
 
        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 
        #endregion 
 
        #region IBowler 
        public double BowlerSpeed { get; set; } 
        public string BowlerType { get; set; }  
        public int BowlerBallsBowled { get; set; } 
        public int BowlerMaidens { get; set; } 
        public int BowlerWickets { get; set; } 
        public double BowlerEconomy => BowlerRunsConceded / 
         BowlerOversBowled;  
        public int BowlerRunsConceded  { get; set; } 
        public int BowlerOversBowled { get; set; } 
        #endregion         
    } 
}

Looking back at our AllRounder and Batsman classes, the code is clearly simplified. It is definitely more flexible and is starting to look like a well-constructed set of classes. Give your solution a rebuild and make sure that it is all working.

So now you know how to simplify your .NET Core applications by applying the Single Responsibility principle. If you found this tutorial helpful and you’d like to learn more, go ahead and pick up the book C# 7 and .NET Core Blueprints, authored by Dirk Strauss and Jas Rademeyer.

Read Next

What is ASP.NET Core?

How to call an Azure function from an ASP.NET Core MVC application

How to dockerize an ASP.NET Core application

 


Subscribe to the weekly Packt Hub newsletter. We'll send you the results of our AI Now Survey, featuring data and insights from across the tech landscape.

* indicates required
I'm a technology enthusiast who designs and creates learning content for IT professionals, in my role as a Category Manager at Packt. I also blog about what's trending in technology and IT. I'm a foodie, an adventure freak, a beard grower and a doggie lover.

LEAVE A REPLY

Please enter your comment!
Please enter your name here