Hi there!!! How have you been doing? Are you all right? I hope so!
Today I'm going to talk about a theme that's everyone talks or write about. But sometimes it's difficult to undersand every principle. I'm talking about SOLID.
A lot of people, when I ask about SOLID, propably always remember of the first principle (Single Responsability Principle). But when I ask about another, some people don't remember or feel difficult to explain. AND I UNDERSTAND.
Really, it's difficult to explain, without coding or remender the definition of each principle. But in this article, I want to present each principle on easy way. So I will use Typescript to exemplify.
So let's begin!
Single Responsability Principle - SRP
The easier principle to understand and remember.
When whe are coding, it's easy to identify when we are forgetting the principle.
Let's imagine that we have a TaskManager class:
class TaskManager {
constructor() {}
connectAPI(): void {}
createTask(): void {
console.log("Create Task");
}
updateTask(): void {
console.log("Update Task");
}
removeTask(): void {
console.log("Remove Task");
}
sendNotification(): void {
console.log("Send Notification");
}
sendReport(): void {
console.log("Send Report");
}
}
All right! Probably do you notice thee problem, isnt'it?
The class TaskManager have a lot of responsabilities that don't belong to her. For example: sendNotification and sendReport methods.
Now, let's refact and apply the solution:
class APIConnector {
constructor() {}
connectAPI(): void {}
}
class Report {
constructor() {}
sendReport(): void {
console.log("Send Report");
}
}
class Notificator {
constructor() {}
sendNotification(): void {
console.log("Send Notification");
}
}
class TaskManager {
constructor() {}
createTask(): void {
console.log("Create Task");
}
updateTask(): void {
console.log("Update Task");
}
removeTask(): void {
console.log("Remove Task");
}
}
Simple, isnt'it? We just separete the notification and report in specified classes. Now we are respecting the Single Principle Responsability!
The definition: Each class must have one, and only one, reason to change
.
Open Closed Principle - OCP
The second principle. Also, I consider easy to understand. A tip for you, if you notice that you have a lot of conditions in some method to verify something, perhaps you are in case of the OCP.
Let's imagine the following example of Exam Class:
type ExamType = {
type: "BLOOD" | "XRay";
};
class ExamApprove {
constructor() {}
approveRequestExam(exam: ExamType): void {
if (exam.type === "BLOOD") {
if (this.verifyConditionsBlood(exam)) {
console.log("Blood Exam Approved");
}
} else if (exam.type === "XRay") {
if (this.verifyConditionsXRay(exam)) {
console.log("XRay Exam Approved!");
}
}
}
verifyConditionsBlood(exam: ExamType): boolean {
return true;
}
verifyConditionsXRay(exam: ExamType): boolean {
return false;
}
}
Yeah, propably you already saw this code several times. First we are breaking the first principle SRP and making a lot of conditions.
Now imagine if another type of examination appears, for example, ultrasound. We need to add anonther method to verify and another condition.
Let's refact this code:
type ExamType = {
type: "BLOOD" | "XRay";
};
interface ExamApprove {
approveRequestExam(exam: NewExamType): void;
verifyConditionExam(exam: NewExamType): boolean;
}
class BloodExamApprove implements ExamApprove {
approveRequestExam(exam: ExamApprove): void {
if (this.verifyConditionExam(exam)) {
console.log("Blood Exam Approved");
}
}
verifyConditionExam(exam: ExamApprove): boolean {
return true;
}
}
class RayXExamApprove implements ExamApprove {
approveRequestExam(exam: ExamApprove): void {
if (this.verifyConditionExam(exam)) {
console.log("RayX Exam Approved");
}
}
verifyConditionExam(exam: NewExamType): boolean {
return true;
}
}
Woow, much better! Now if another type of examination appears we just implements the interface ExamApprove
. And if another type of verification for the exam comes up, we only update the interface.
Definition: Software entities (such as classes and methods) must be open for extension but closed for modification
Liskov Substitution Principle - LSP
One of more complicated to understand and examplain. But how I said, I will make easier to you understand.
Imagine that you have an university and two types of students. Student and Post Graduated Student.
class Student {
constructor(public name: string) {}
study(): void {
console.log(`${this.name} is studying`);
}
deliverTCC() {
/** Problem: Post graduate Students don't delivery TCC */
}
}
class PostgraduateStudent extends Student {
study(): void {
console.log(`${this.name} is studying and searching`);
}
}
We have a problem here, we are extending of Student, but the Post Graduated Student don't need to deliver a TCC. He only study and search.
How we can resolve this problem? Simple! Let's create a class Student and separete the Student of graduation and Post Graduation:
class Student {
constructor(public name: string) {}
study(): void {
console.log(`${this.name} is studying`);
}
}
class StudentGraduation extends Student {
study(): void {
console.log(`${this.name} is studying`);
}
deliverTCC() {}
}
class StudentPosGraduation extends Student {
study(): void {
console.log(`${this.name} is studying and searching`);
}
}
Now we have a better way to approach to separete their respective responsabilities. The name of this principle can be scary but its principle is simple.
Definition: Derived classes (or child classes) must be able to replace their base classes (or parent classes)
Interface Segregation Principle - ISP
To understand this principle, the trick is remember of the definition. A class shound not be forced to implement methods that will not be used.
So imagine that you have a class the implement a interface that its never be used.
Let's imagine a scenario with an Seller and a Recepsionist of some shop. Both seller and recepsionist have a sallary, but only a seller have a commission.
Let's see the problem:
interface Employee {
salary(): number;
generateCommission(): void;
}
class Seller implements Employee {
salary(): number {
return 1000;
}
generateCommission(): void {
console.log("Generating Commission");
}
}
class Receptionist implements Employee {
salary(): number {
return 1000;
}
generateCommission(): void {
/** Problem: Receptionist don't have commission */
}
}
Both implements the Employee interface, but the receptionist don't have comission. So we are force to implement a method that never it will be used.
So the solution:
interface Employee {
salary(): number;
}
interface Commissionable {
generateCommission(): void;
}
class Seller implements Employee, Commissionable {
salary(): number {
return 1000;
}
generateCommission(): void {
console.log("Generating Commission");
}
}
class Receptionist implements Employee {
salary(): number {
return 1000;
}
}
Easy beasy! Now we have two interfaces! The employer class and the comissionable interface. Now only the Seller will implement the two interfaces where it will have the commmission. The receptionist don't only implements the employee. So the Receptionist don't be forced to implement the method that will never be used.
Definition: A class should not be forced to implement interfaces and methods that will not be used.
Dependency Inversion Principle - DIP
The last one! By name you can think that is hard to remember! But probably you already see this principle every time.
Imagine that you have a Service class that integrates with a Repository class that will call the Database, for example a Postgress. But if the repository class change and the database change for a MongoDB, for example.
Let's see the example:
interface Order {
id: number;
name: string;
}
class OrderRepository {
constructor() {}
saveOrder(order: Order) {}
}
class OrderService {
private orderRepository: OrderRepository;
constructor() {
this.orderRepository = new OrderRepository();
}
processOrder(order: Order) {
this.orderRepository.saveOrder(order);
}
}
We notice that the repository is OrderService class is directly coupled to the concrete implementation of OrderRepository class.
Let's refact this example:
interface Order {
id: number;
name: string;
}
class OrderRepository {
constructor() {}
saveOrder(order: Order) {}
}
class OrderService {
private orderRepository: OrderRepository;
constructor(repository: OrderRepository) {
this.orderRepository = repository;
}
processOrder(order: Order) {
this.orderRepository.saveOrder(order);
}
}
Nice! Much better! Now we receive the repository as parameter on the constructor to instanciate and use. Now we depend of the abstraction and we don't need to know what repository we are using.
Definition: depend on abstractions rather than concrete implementations
Finishing
So how you feeling now? I hope that with this easy examples you can remember and understand what and why to use this principles in your code. It makes easier to undestand and scale, besides you are applying clean code.
I hope you that you liked!
Thank you so much and stay well always!
Contacts:
Linkedin: https://www.linkedin.com/in/kevin-uehara/
Instagram: https://www.instagram.com/uehara_kevin/
Twitter: https://twitter.com/ueharaDev
Github: https://github.com/kevinuehara
dev.to: https://dev.to/kevin-uehara
Youtube: https://www.youtube.com/@ueharakevin/
Top comments (25)
Hi, thank you for the article and the effort to explain complicated concept.
To complete your article, there is another way to interpret the Single Responsability (which extends your explanation). "One class must change for only one reason". For exemple (pseudocode)
You should split it in two classes, because actually your class manage two concepts : The user account on the platform (with connexion, password, etc.) and the client who have commands on the platform (with list of commands, address, all related to commands). Even if there are some duplicated information, there are two different thing:
For the LSP, which is really complicated, I totally agree with you, it's very complicated to explain. Your explanation miss an important concept reading "A child class must replaced parent class without break the program". We can understand this rules this way "Child class must not be more restrictive than a parent class" or "A child class must accept all the parent class can do (method signature and returns)". For exemple :
If you do that the program could be broken.
Any way, thank you for your explanation this was just to complete your explanation.
And don't forget about principles:
Sometimes they are very important too 👍
I really appreciate that you're posting and making the information accessible, but this is painful to read. The ideas here are laid out well, but I can't ignore how terrible the language use is and I had to ask GPT to make it readable for me.
Please use a tool such as Grammarly or any number of AI tools to help write clear and understandable English.
I don't mean to discourage you, in fact, I think you're doing a great job. Half the battle is showing up, and you're already here and making the posts. Using those tools can help you to improve your writing skills by taking their corrections into consideration. It's how I continue to improve my own writing.
Hi Kevin Toshihiro Uehara,
Your tips are very useful
Thanks for sharing
Really great article, simple and understandable.
the tips is so cool.
but .. a lot of spelling errors. (my english is not so good)
The best explanation of this, to share with my partner's team. Thank you!!! :')
Good job @kevin-uehara . amazing explanation :)
Thanks!
Hi Kevin.
Another BrazDev here!
I've fascinating with your courage in writing a article in plain english!
Thanks for encouraging me as well!