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.
[box type=”shadow” align=”” class=”” width=””]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.[/box]
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:
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:
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.
Your Solution should now look as follows:
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
How to call an Azure function from an ASP.NET Core MVC application
How to dockerize an ASP.NET Core application