The misunderstanding of Single Responsibility, the first of the SOLID principles, is primarily due to its name. Most programmers do not fully understand this principle, assuming a responsibility corresponding to their name without going into the depth of the matter. But let’s see if this is so.
Single Responsibility is not a responsibility, but a principle of changing for a reason.
Responsibility vs a Reason for a Change.
The concept of responsibility is often a static concept, independent of context. This concept does not allow us to dynamically vary by context. Responsibility can act as 1 change or multiple changes in context. It depends on the requirements.
Say you have a Database class,
public class Data {}
internal class Database {
public async Task < IEnumerable < Data >> ReadAllAsync() {}
public async Task < Data > GetByIdAsync(int id) {}
public async Task < int > WriteAsync(Data data) {}
public async Task < int > WriteAllAsync(Data data) {}
public async Task < Data > UpdateAsync(int id, Data data) {}
public async Task < Data > DeleteAsync(int id) {}
public async Task < Data > DeleteAllAsync(int[] id) {}
}
As you can see, the Database class encapsulates the database operations. Here we can say that the Database class has one single responsibility. But how can we tell whether any class has single responsibility based on SRP? Of course, it depends on whether it will be changed for a reason within the context. But does this rule apply to the database class?
Partially. Because the Database class here seems to be changed for some reason without being context-dependent. But depending on the REQUIREMENTS, it can carry one or more responsibilities. If the work related to the Database class depends on a specific person, department, or in general use under program control, etc. then this design is correct. So, the key concept that makes this design fail is that it is tied to a specific few people or departments.
Let’s say that we are developing a program, and there, the database delete behaviors are under the control of the BI department, the restore behaviors are under the control of the Security department, and the rest of the work is related to the accounting department. Then the class we already had more than 1 responsibility. Named functionalities above, should be assembled in separate classes.
But what happens if we keep the Database class unchanged?
A class has more than 1 responsibility within this context
There is more than one reason to change class
The change of one department has a direct impact on the functionality of other departments and can even disrupt their functionality
Class testing becomes difficult because departments have different logic.
Class reuse becomes difficult
Class dependency problems arise. If each department requires different dependencies, the class already loses its encapsulation unit
The problem of class maintenance appears.
SRP directly encapsulates the logic of high cohesion and loosely coupled. Gather together those things that change for the same reason, and separate those things that change for different reasons.
So what does SRP serve?
The main point of service of SPR is to deal with complexity.
What do we mean by dealing with complexity?
If 1 class has more than 1 responsibility (one reason to change), then it becomes very difficult to change that class and add new functions to it. The human brain can only focus on one thing at the same time, and it’s hard to do anything in a multitasking mode. At least we worry about how the new function we write or the function we change will affect the others.
As a preliminary conclusion, we can say that SRP is a principle aimed to create simple classes (which change for 1 reason depending on the context).
But can 1 class have several areas of responsibility?
Of course. It is permissible if each of these responsibilities is simple. In most cases, this is a feature found in orchestrator classes. Orchestrator classes encapsulate other classes and act as mediators. Their main purpose is to act as a bridge of communication between other classes.
internal class ReportManager {
private readonly ReportGenerator _reportGenerator;
private readonly IReportService _reportService;
private readonly IReportAnalyzer _reportAnalyzer;
public ReportManager(IReportService reportService, IReportAnalyzer reportAnalyzer) {
_reportService = reportService;
_reportAnalyzer = reportAnalyzer;
}
public async Task < ReportData > GenerateReportAsync() {
var reportInfo = _reportService.GetReportInfo();
ReportData[] analyzedData = _reportAnalyzer.AnalyzeData(reportInfo);
Logger.LogData(reportInfo);
return _reportGenerator.GenerateReport(analyzedData);
}
}
In the example above, our Orchestrator class (ReportManager)
Receives data from the service
Analyzes it
Writes to the log
Generates a report.
Although it seems that this class does several jobs what we have listed above are parts of a single job and are considered as one job. Because according to high cohesion, when one of the above steps changes, the logic of the program must change, and they are tightly coupled with each other. Each of the steps performed by the ReportGenerator is a simple step that encapsulates the work. The main purpose of ReportGenerator is to be an orchestrator.
However, sometimes it is necessary to divide classes that do 1 job into several parts. But what causes it?
SRP not only serves to separate functionality that does different work, with multiple reasons for the change but also subclasses classes that do 1 job depending on the complexity of the work done.
Example
Let’s say that we need to apply business rules to the data we receive from any X service and generate them as a report. The report generation process itself can be difficult. For example, when generating a report, it parses it based on a certain algorithm, and this algorithm requires separate monitoring. Then we have to create a separate class called ReportParser.
internal class ReportGenerator {
public ReportData GenerateReport(ReportData[] analyzedData) {
//parse report
//generate data
throw new NotImplementedException();
}
}
//more complex ReportGenerator
internal class ReportGenerator2 {
private readonly ReportParser _reportParser;
public ReportGenerator2(ReportParser reportParser) {
_reportParser = reportParser;
}
public ReportData GenerateReport(ReportData[] analyzedData) {
_reportParser.Parse(analyzedData);
//generate data
throw new NotImplementedException();
}
}
class ReportParser {
public ReportData[] Parse(ReportData[] data) {
throw new NotImplementedException();
}
}
That is, SRP is not only suitable for separating independent functionalities, but also the functionalities that serve a task, as the complexity of the program increases, it is necessary to break them into parts.
Want to dive deeper?
Every 5 days, I share my senior-level expertise on my DecodeBytes YouTube channel, breaking down complex topics like .NET, Microservices, Apache Kafka, Javascript, Software Design, Node.js, and more into easy-to-understand explanations. Join us and level up your skills!