Hi, This is my first article on this site. This is my first article at all. So, in this series of articles, I want to review SOLID with Typescript. To start this series, of course, I want to talk about the S of SOLID (Single responsible principle). Uncle Bob says in the book Clean Architecture, 2019: "A module should be responsible to one, and only one, actor." This is because the name of the principle causes a lot of mistakes for programmers; they think something like that: "A class should do one thing."
But I think in this case, the correct answer is something like that: A class should only do one thing related to the actor or change only when this actor requests a change. The actor is related to users or stakeholders, and that's the point: a software or class only needs to change when these actors request a change.
So, here we can see several problems that we can solve by using that principle, like:
- Lack of cohesion.
- High coupling.
- Testing is hard.
- Reusing of code is too hard.
Let's go to examples.
Imagine a class that emits reports.
class ReportService {
constructor(private apiEndpoint: string) {}
public getReport(): any {
const salesReport = this.apiRequest('/sales');
const sellersReport = this.apiRequest('/sellers');
const billingsReport = this.apiRequest('/billings');
return {
salesReport,
sellersReport,
billingsReport,
};
}
private apiRequest(endpoint: string): any {
// Request
}
}
In this case, the class emits three reports: Sales, Sellers and billings. In this class, we have a method to request a report from the API and one method to export the report requested.Take a look; we have three actors here: The financial sector, the people sector, and a sector of sales. Imagine if one of these actors requests a change; all the classes need to change at the same time. And this causes a lot of work.
Take note: To solve this problem, we need to split responsibility. Making three classes, one for each actor.
Examples:
class salesReportService{
constructor(private apiEndpoint: string) {}
public getReport(): any {
return this.apiRequest('/sales');
}
private apiRequest(endpoint: string): any {
// Request
}
}
class sellersReportService{
constructor(private apiEndpoint: string) {}
public getReport(): any {
return this.apiRequest('/sellers');
}
private apiRequest(endpoint: string): any {
// Request
}
}
class billingsReportService{
constructor(private apiEndpoint: string) {}
public getReport(): any {
return this.apiRequest('/billings');
}
private apiRequest(endpoint: string): any {
// Request
}
}
Well. I think that's it. Sorry for any error, and feel free to correct me any time.
Top comments (7)
You did not mention derived classes, but the SPR is a strong argument to use them. Assume you have a range of objects or functions all doing similar things. If you write all members of the group separately, you probably need to implement the same code multiple times. If you call a subfunction, it might be hard to meet the different requirements.
If you put the code in an abstract base class, all members of the group will inherit the code but may apply modifications where necessary. Then you will only have one class that is responsible for the behavior of the whole group.
@efpage you see, derived classes is not about SRP.
The inheritance is one of the ways to follow OCP (Open/Close Principle) and DIP (Dependency Inversion Principle). And more than -- it is not a best way to respect these principles.
Inheritance is a mechanism of sub-typing, limitted by LSP (Liskov Substitution Principle) that makes SRP not relevant to inheritance: Derived Class MUST DO the same stuff as a Base Class, but in differnet way, preserving safity of substitution.
Oh, it's an excellent idea. This text is just my initial annotations, And I never thought so many people would reach me lol. Maybe I can write some text another time, going deeper into the topic.
The topic is much bigger than it seems.
Many systems like the Windows GDI or the android API are build using excessive inheritance. Such systems can contain hundreds of classes, and management would be nearly impossible without a clear structure. Methods introduced are always introduced as early as possible to assure, they are only implemented once.
Here is an image of the android view hierarchy.
@witerlland actually inheritance is not too much good idea to understand or present SRP.
See my comment: dev.to/dscheglov/comment/2cob9
Look, all developers are reflecting SOLID. To write such texts is your way. And it is a really good approach to do that.
Just try to understand comments and discuss it if there is something unclear.
@witerlland SRP (Single Responsibility Principle) is a rather controversial story )
It seems your example is not too much suitable )
On the start you have code that emits a single report that contains three sub-reports, it is not exactly you have after the refactoring: just three reports
I can guess you mean something like that:
If so, we cannot decide to split or not to split just considering the naming, and guessing that "Actors" are different. But yes, in some cases we have to split this class, in some other cases -- we can keep it as a single one.
So, both works. To make final decision we need to deep dive to the problem details.
However, in general, programs live longer then organization charts ;)
Regarding the SRP.
One of the most important criteria for SRP is "Concern Separation": The class that builds a report should not warry about how to send data.
The method
sendApiRequest
must be extracted to the separate classApiClient
,and correspondent instance must be injected into the ReportService class or classes:
See more detailed example on TS Playground
One small suggestion: if you want to follow SOLID -- think in interfaces, not in classes
Oh, it's an excellent idea. And a better way to explain the concept. Thanks for correcting me and giving me examples of it, because it's just an initial annotation and I want to go deeper into the theme.