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

Collapse
 
yannschepens profile image
Yann Schepens • Edited

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)

class Client {
    String id;
    String password;
    String name;
    String address;
    String lastCommand;
    String firstConnection;
}
Enter fullscreen mode Exit fullscreen mode

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:

class User {
    String id;
    String password;
    String firstConnection;
}

class Client {
    String id;
    String name;
    String address;
    String lastCommand;
    String firstConnection;
}
Enter fullscreen mode Exit fullscreen mode

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 :

interface User{}
class Client implements User{}
class Admin implements User{}

class MessageSender {
    sendMessageToUser(User u) {}
}

class NiceMessageSender extends MessageSender {
    sendMessageToUser(User u) {
        if (u instanceof Admin) {
            thrown new Exception("Could not send nice message to admin");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you do that the program could be broken.

Any way, thank you for your explanation this was just to complete your explanation.

Collapse
 
jon117 profile image
Jon-117

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.

Collapse
 
alexlevn profile image
Alex Lee

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

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
 
wahidnabi_70 profile image
wahid-Nabi

Really great article, simple and understandable.

Collapse
 
jangelodev profile image
João Angelo

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

Collapse
 
w0lf46 profile image
Lucas Palhano

Thanks!

Collapse
 
hectorlaris profile image
Héctor Serrano

Tks Kevin.
The difficult thing is to be simple!

Collapse
 
akashj2342 profile image
Akash

Great article.
Easy to understand!

Collapse
 
clipso profile image
Clipso

Good job @kevin-uehara . amazing explanation :)