Introduction
C# is an object-oriented programming language. These days whenever you talk about object-oriented programming you hear the acronym, SOLID. These are five design principles introduced by Michael Feathers to make our object-oriented applications easy to understand, maintain and expand as future requirements change. Today, we will look at the first principle with an example and in the following articles we will cover the remaining principles with examples of each.
The SOLID principles
There are five principles to follow to ensure our application meets the SOLID requirements. These are as below,
- Single Responsibility Principle (SRP)
- Open Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
The Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) states that a class should have one single piece of responsibility in the application. It should have only one reason to change and that is if the single piece of responsibility needs a change. This will ensure the class and ultimately the whole application is very robust and easy to maintain and expand, if required. Let us look at this with an example,
Let us create a new .NET Core 3.1 console application in Visual Studio 2019 Community Edition as below,
Inside this project, I have created a new class file “SingleResponsibilityPrinciple”. In this file, I create the following “Student” class.
-
-
- public class Student
- {
- public string ID { get; set; }
- public string Name { get; set; }
- public string Email { get; set; }
- public float PaperAMarks { get; set; }
- public float PaperBMarks { get; set; }
- public float PaperCMarks { get; set; }
- public float BasicTution { get; set; }
- public float OtherFees { get; set; }
-
- public float CalculateMarksAverage()
- {
- return (PaperAMarks + PaperBMarks + PaperCMarks) / 3;
- }
-
- public float CalculateFees()
- {
- return BasicTution + OtherFees;
- }
- }
This class does not follow the “Single Responsibility Pattern” as we are adding the marks calculation and fees calculation into the main student class. Hence, if there is any need to change anything in the fees calculation, we would need to change the main student class and then unit test all the three components including the main student class, the marks calculator class and the fees calculator class. Hence, we have not given a single responsibility to the student class.
We can fix this as below,
-
-
- public interface IStudentMarksCalculator
- {
- float PaperAMarks { get; set; }
- float PaperBMarks { get; set; }
- float PaperCMarks { get; set; }
- float CalculateMarksAverage();
- }
-
- public class StudentMarksCalculator : IStudentMarksCalculator
- {
- public float PaperAMarks { get; set; }
- public float PaperBMarks { get; set; }
- public float PaperCMarks { get; set; }
- public float CalculateMarksAverage()
- {
- return (PaperAMarks + PaperBMarks + PaperCMarks) / 3;
- }
- }
-
- public interface IStudentFeesCalculator
- {
- float BasicTution { get; set; }
- float OtherFees { get; set; }
- float CalculateFees();
- }
-
- public class StudentFeesCalculator: IStudentFeesCalculator
- {
- public float BasicTution { get; set; }
- public float OtherFees { get; set; }
- public float CalculateFees()
- {
- return BasicTution + OtherFees;
- }
- }
-
- public class StudentFixed
- {
- public string ID { get; set; }
- public string Name { get; set; }
- public string Email { get; set; }
-
- private readonly IStudentMarksCalculator _studentMarksCalculator;
- private readonly IStudentFeesCalculator _studentFeesCalculator;
-
- public StudentFixed(IStudentMarksCalculator studentMarksCalculator, IStudentFeesCalculator studentFeesCalculator)
- {
- _studentMarksCalculator = studentMarksCalculator;
- _studentFeesCalculator = studentFeesCalculator;
- }
-
- public float GetMarks(float A, float B, float C)
- {
- _studentMarksCalculator.PaperAMarks = A;
- _studentMarksCalculator.PaperBMarks = B;
- _studentMarksCalculator.PaperCMarks = C;
- return _studentMarksCalculator.CalculateMarksAverage();
- }
-
- public float GetTution(float Basic, float Others)
- {
- _studentFeesCalculator.BasicTution = Basic;
- _studentFeesCalculator.OtherFees = Others;
- return _studentFeesCalculator.CalculateFees();
- }
-
- }
In the above code, we see that we have created two classes. One for calculating the fees of the student and the other to calculate the marks of the student. Both are injected into the main student class using dependency injection, which we will look into when we discuss the last SOLID principle called dependency inversion. Hence, any changes required in the fees calculation or marks calculation classes can be done independently and would not impact the main student class if the interfaces of these classes do not change.
If we run both the implementations above, we get the same results, but one has applied the SRP correctly and the other has not.
-
-
- var student = new Student();
-
- student.PaperAMarks = 75.5F;
- student.PaperBMarks = 81F;
- student.PaperCMarks = 79.5F;
-
- Console.WriteLine($"The student's average marks are {student.CalculateMarksAverage()}");
-
- student.BasicTution = 1500F;
- student.OtherFees = 255.75F;
-
- Console.WriteLine($"The student's Fees is {student.CalculateFees()}");
-
-
-
- var studentFixed = new StudentFixed(new StudentMarksCalculator(), new StudentFeesCalculator());
-
- Console.WriteLine($"The student's average marks are {studentFixed.GetMarks(75.5F, 81F, 79.5F)}");
- Console.WriteLine($"The student's Fees is {studentFixed.GetTution(1500F, 255.75F)}");
Summary
In this article, we have looked at implementing the Single Responsibility Pattern (SRP) in a practical example. I would recommend you look though your existing classes and identify places where you have violated this principle and then think of ways to fix it. This will help to get you thinking in terms of applying this principle and help you to apply it to your code in the future as well. In my next article, we will look at the Open Closed Principle.