The SOLID Principles from Wider Angle — SRP
SOLID is an acronym for five design principles intended to make object-oriented class designs more flexible, reusable, understandable and maintainable. Robert C. Martin first coined these principles which was later brought to light by many people.
Many articles have covered these topics and pointed out these principles with a common perspective — The OOP.
These principles are generally looked at from the perspective of object-oriented class designs. But if we closely look, they are applied in many software paradigms such as API Design, Architecture, User Interface Diagrams, and Microservice designs. This article is an attempt to point out where else these principles can be applied other than in the object-oriented paradigm.
Here are the five design principles.
- SRP — Single Responsibility Principle
- OCP — Open-Closed Principle
- LSP — Liskov Substitution Principle
- ISP — Interface Segregation Principle
- DIP — Dependency Inversion Principle
In this article, we will look at Single Responsibility Principle(SRP) from a different perspective. There will be separate articles for other principles. This article concentrates on SRP.
Single Responsibility Principle (SRP)
The Single Responsibility Principle suggests that every component should have one single responsibility. Here we need to understand these two terms: Component and Responsibility.
Component: As I said earlier in this article that these principles apply everywhere. So in that context, a component can be anything such as a module, a class, a microservice, a library, or even a function.
Responsibility: The term Responsibility refers to a “reason to change”. That means, according to this principle, a component, be it a class or a module or microservice, should have only one reason to change.
Let’s consider the below Java class Patient that holds the patient details, saves the record to the database and exports the patient ID card as Word or PDF.
class Patient {
private int id;
private String name;
// .....
// .....
// Getters & Setters
public void save() {
// save this patient in database
}
public void exportPatientIDCardToWord() {
// Export Patient ID Card to Word
}
public void exportPatientIDCardToPDF() {
// Export Patient ID card to PDF
}
}Let's first look at the responsibilities of the above class.
- First, it holds the details of the patient and provides the read-write interface through getters and setters.
- Second, it saves the details of the patient in a database.
- Third, it exports the details of the patient as an ID card in Word or PDF format.
So we have the above three responsibilities so there are three reasons that this class may need to be changed in the future.
- One or more fields get added or deleted.
- The underlying database may get changed which forces the implementation of the
save()method to be changed. - There may be another requirement that the Patient ID card may need to be exported as a PNG.
So clearly this class has multiple reasons to change and thus violates the Single Responsibility Principle.
But what is the problem here anyway? After all, we are going to add some new code or modify/delete the existing code. How does that become a problem?
Well, there are several reasons in terms of flexibility, reusability, and maintainability and here are they.
- Let's look at the
save()method. If we switch from MySQL to Oracle SQL then the whole method needs to be changed. After we changed that implementation to Oracle, what if we want to return to MySQL in future, maybe, because Oracle is costing more? This is possible, right? Then do we rewrite the old code again? That is a pathetic way of maintaining the source code. You may be thinking that we can consider writing the methods such assaveToMySQL(),saveToOracle()for MySQL and Oracle implementation respectively. The same is the case withExporterfor which you may want to consider writing an extra method such asexportToPNG(). - The above approach of adding separate methods for each implementation comes with its own problem. If we keep on adding methods for additional responsibilities in the same class, the class gets exploded making it less flexible and reusable, therefore and not maintainable. Also, it is very tough to write the unit tests for these kinds of classes.
Please note that every piece of code that we write may introduce new bugs. So what needs to be done for the above class to be aligned with Single Responsibility Principle?
The simple rule is to separate the responsibilities into different classes.
NOTE: As a starting point, divide the responsibilities into separate classes and then proceed from there.
For example, we can create different classes for different responsibilities such as below:
class Patient {
private int id;
private String name;
// .....
// .....
// Getters & Setters
}
class PatientRepository {
public void saveToMySql(Patient patient) { ... }
public void saveToOracle(Patient patient) { ... }
public void saveToMongo(Patient patient) { ... }
// ...
}
class Exporter {
public void toPDF() { ... }
public void toWord() { ... }
public void toPNG() { ... }
// ...
}- The
Patientclass just to deal with the read/write operations with getters and setters. - The
PatientRepositoryclass that deals with database operations. - The
Exporterclass that deals with exporting to PDF/Word/PNG.
Now the Patient class looks clean. It now follows the Single Responsibility principle.
What about the other two classes that are newly introduced? Are they following SRP? Well, they are not. The PatientRepository and Exporter classes still suffer the same class exploding problem, right?
The PatientRepository class has three responsibilities now and may get extended to other responsibilities. The same is the case with Exporter class.
So let's further improve this design using the same rule again, which is, to separate out the responsibilities in different classes.
interface PatientRepository {
public void save(Patient patient);
}
class MySqlPatientRepository implements PatientRepository {
@Override public void save(Patient patient) { ... }
}
class OracleSQLPatientRepository implements PatientRepository {
@Override public void save(Patient patient) { ... }
}
class MongoPatientRepository implements PatientRepository {
@Override public void save(Patient patient) { ... }
}
interface Exporter {
public void export(Patient patient);
}
class PDFExporter implements Exporter {
@Override public void export(Patient patient) { ... }
}
class WordDocExporter implements Exporter {
@Override public void export(Patient patient) { ... }
}
class PNGExporter implements Exporter {
@Override public void export(Patient patient) { ... }
}In the above code snippet, we changed the PatientRepository and Exporter classes as interfaces and we implemented concrete classes based on the responsibility. This looks cleaner and more flexible now. This makes every class follow Single Responsibility Principle.
Single Responsibility Principle in Microservices
So far we looked at the Single Responsibility Principle in the view of Object Oriented Paradigm. Let’s now look at it from a microservice perspective.
Let us say we have three entities to deal with, viz, Patient, Doctor, and Hospital. So we need to design a system that captures the data for all these three entities. In Domain Driven Design(DDD ), which is closely related to Microservice design, these entities are taken as Boundary Context. And each Boundary Context is implemented as a microservice. In this case we will have three microservices patient-service, doctor-service, hospital-service with their own databases. If the patient-service wants to know a particular doctor’s information it has to send a request (usually a HTTP request) to doctor-service for it. So that means each service is following the Single Responsibility Principle. The patient-service only stores the patient data, doctor-service only stores doctor data and the hospital-service of course stores the hospital data. This makes the microservice less coupled and highly cohesive.
That’s all in this article, in the next article we will look at the other principles.
Stay Tuned until then. And let me know if you have any questions or improvements or any other concepts that you want to learn in the comments section. If any corrections(typos or conceptual) please highlight them as well. Because nobody is perfect. And I will be very happy to receive negative comments as well if they help in improving these articles. Together we may understand it better.
*** Follow me on LinkedIn. Have a Happy Learning. :) ***
*** Together we may understand it better. ***
If you liked this content and want to read more like this, consider supporting me and thousands of other writers by signing up for a membership. It only costs $5 a month giving you unlimited access to stories on Medium. You even have a chance to make money with your own writing as well. If you sign up using my link, I’ll be supported directly with a small portion of your fee. If you do so, thank you plenty!
