DEV Community

Cover image for SOLID - The Simple Way To Understand
Kevin Toshihiro Uehara
Kevin Toshihiro Uehara

Posted on

SOLID - The Simple Way To Understand

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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  */
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 (17)

Collapse
 
jangelodev profile image
João Angelo

Hi Kevin Toshihiro Uehara,
Your tips are very useful
Thanks for sharing

Collapse
 
wahidnabi_70 profile image
wahid-Nabi

Really great article, simple and understandable.

Collapse
 
hectorlaris profile image
Héctor Serrano

Tks Kevin.
The difficult thing is to be simple!

Collapse
 
gabrielsimas profile image
Gabriel Simas

Hi Kevin.
Another BrazDev here!

I've fascinating with your courage in writing a article in plain english!

Thanks for encouraging me as well!

Collapse
 
devh0us3 profile image
Alex P

And don't forget about principles:

  • DRY – do not repeat yourself
  • WET – write everything twice

Sometimes they are very important too 👍

Collapse
 
akashj2342 profile image
Akash

Great article.
Easy to understand!

Collapse
 
msindev profile image
Mohit Singh

The examples make the concepts very easy to understand.

Collapse
 
ahmedalmogy profile image
Ahmedalmogy

neat explain ,thank you , waiting for more articles such that .

Collapse
 
masteing_the_code profile image
Joel Jose

Nice one. Its so easy to understand.

Collapse
 
alexlevn profile image
Alex Lee

the tips is so cool.
but .. a lot of spelling errors. (my english is not so good)

Collapse
 
ibrito profile image
isaias

Thank you for simplifying the understanding of SOLID principles.

Collapse
 
kishan_vp_66a386dcc65935 profile image
Kishan V P

Thank you for all great posts !

Collapse
 
clipso profile image
Clipso

Good job @kevin-uehara . amazing explanation :)

Collapse
 
meharsuleiman profile image
mehar sulaiman

Love to read

Collapse
 
decker67 profile image
decker

And with using JavaScript and simple Objects you can do this a lot easier. No need for classes and inheritance and the like.