SOLID: Single Responsibility Principle
The Single Responsibility Principle is the first principle of SOLID principles. It is the fundamental principle of object-oriented programming that determines how we should design classes.
The Single Responsibility Principle states that:
In other words, a class should have only one responsibility and therefore it should have only one reason to change its code. If a class has more than one responsibility, then there will be more than one reason to change the class (code).
Now, the question is what is responsibility?
An application can have many functionalities (features). For example, an online e-commerce application has many features such as displaying product lists, submitting an order, displaying product ratings, managing customers' shipping addresses, managing payments, etc. Along with these features, it also validates and persists products and customers' data, logs activities for auditing and security purposes, applies business rules, etc. You can think of these points as functionalities or features or responsibilities. Change in any functionality leads to change in the class that is responsible for that functionality.
Let's check how many responsibilities the following Student
class has:
public class Student
{
public int StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DoB { get; set; }
public string email { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
public void Save()
{
Console.WriteLine("Starting Save()");
//use EF to save student to DB
Console.WriteLine("End Save()");
}
public void Delete()
{
Console.WriteLine("Starting Delete()");
//check if already subscribed courses then don't delete
Console.WriteLine("End Delete()");
}
public IList<Course> Subscribe(Course cs)
{
Console.WriteLine("Starting Subscribe()");
//apply business rules based on the course type
if(cs.Type == "online")
{
//validate
}
else if(cs.type == "live")
{
}
//payment processing code
//save course subscription to DB
//send email confirmation code
Console.WriteLine("End Subscribe()");
}
}
The above Student
class has the following responsibilities:
-
Holds student's properties such as
StudentId
,FirstName
,LastName
, andDoB
. - Save a new student, or update an existing student to a database.
- Delete existing students from the database if not subscribed to any course.
- Apply business rules to subscribe to courses based on the course type.
- Process the payment for the course.
- Send confirmation email to a student upon successful registration.
- Logs each activity to the console.
If anything in the above responsibility changes, then we will have to modify the Student
class.
For example, if you need to add a new property then we need to change the Student
class. Or, if you need a change in the database, maybe moving from a local server to a cloud, then you need to change the code of the Student
class.
Or, if you need to change the business rules (validation) before deleting a student or subscribing to a course, or change the logging medium from console to file, then in all these cases you need to change the code of the Student
class.
Thus, you have many reasons to change the code because it has many responsibilities.
SRP tells us to have only one reason to change a class.
Let's change the Student
class considering SRP where we will keep only one responsibility for the Student
class and abstract away (delegate) other responsibilities to other classes.
Start with each responsibility mentioned above and decide whether we should delegate it to other classes or not.
-
The
Student
class should contain all the properties and methods which are specific to the student. Except for theSubscribe()
method, all the properties and methods are related to the student, so keep all the properties. -
The
Save()
andDelete()
method is also specific to a student. Although, it uses Entity Framework to do the CRUD operation which is another reason to change theStudent
class. We should move the underlying EF code to another class to do all DB operations e.g.StudentRepository
class should be created for all CRUD operations for theStudent
. This way if any changes on the DB side then we may need to change only theStudentRepository
class. -
The
Subscribe()
method is more suitable for theCourse
class because a course can have different subscription rules based on the course type. So it is idle to move theSubscribe()
method to theCourse
class. -
Sending confirmation emails is also a part of the
Subscribe()
method, so it will be a part of theCourse
class now. Although, we will create a separate classEmailManger
for sending emails. -
Here, all the activities are logged on the console using the hard-coded
Console.WriteLine()
method. Any changes in the logging requirement would cause theStudent
class to change. For example, if the admin decides to log all activities in the text file then you need to change theStudent
class. So, it's better to create a separateLogger
class that is responsible for all the logging activities.
Now, look at the following classes redesigned after applying SRP using the above considerations for SRP.
public class Student
{
public int StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DoB { get; set; }
public string email { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
public void Save()
{
Logger.Log("Starting Save()");
_studentRepo.Save(this);
Logger.Log("End Save()");
}
public void Delete()
{
Logger.Log("Starting Delete()");
//check if already subscribed courses
_studentRepo.Delete(this);
Logger.Log("End Delete()");
}
}
Public class Logger
{
Public static void Log(string message)
{
Console.WriteLine(message);
}
}
public class StudentRepository()
{
Public bool Save(Student std)
{
Logger.log("Starting Save()");
//use EF to add a new student or update existing student to db
Logger.log("Ending Saving()");
}
public bool Delete()
{
Logger.log("Starting Delete()");
//use EF to delete a student
Logger.Log("Ending Delete()");
}
public bool SaveCourse(Student std, Course cs)
{
Logger.log("Starting SaveCourse()");
//use EF to save a course for a student
Logger.Log("Ending SaveCourse()");
}
}
public class Course
{
public int CourseId { get; set; }
public string Title { get; set; }
public string Type { get; set; }
public void Subscribe(Student std)
{
Logger.Log("Starting Subscribe()");
//apply business rules based on the course type live, online, offline, if any
if (this.Type == "online")
{
//subscribe to online course
}
else if (this.Type == "live")
{
//subscribe to offline course
}
// payment processing
PaymentManager.ProcessPayment();
//create CourseRepository class to save student and course into StudentCourse table
// send confirmation email
EmailManager.SendEmail();
Logger.Log("End Subscribe()");
}
}
Public class EmailManager
{
Public static void SendEmail(string recEmailed, string senderEmailId, string subject, string message)
{
// smtp code here
}
}
Public class PaymentManger
{
Public static void ProcessPayment()
{
//payment processing code here
}
}
Now, think about the above classes. Each class has a single responsibility. The Student
class contains properties and methods specific to the student-related activities. The Course
class has course-related responsibilities. The StudentRepository
has responsibilities for student-related CRUD operations using Entity Framework. The Logger
class is responsible for logging activity. The EmailManager
class has email-related responsibilities. The PaymentManager
class has payment-related activities.
In this way, we have delegated specific responsibilities to separate classes so that each class has only one reason to change. These increase cohesion and loose coupling.
Separation of Concerns
The Single Responsibility Principle follows another principle called Separation of Concerns.
Separation of Concerns suggests that the application should be separated into distinct sections where each section addresses a separate concern or set of information that affects the program. It means that high-level business logic should avoid dealing with low-level implementation.
In our example, we separated each concern into separate classes. We had only one Student
class initially, but then we separated each concern like CRUD operations, logging, email, etc. into separate classes.
Thus, the Student
class (high-level class) does not have any idea how CRUD or sending emails is happening. It just use the appropriate method and that's it.
The SRP and Separation of Concerns principle increase cohesion and loose coupling.