DEV Community

Juan Carlos Feris Gómez
Juan Carlos Feris Gómez

Posted on

The SOLID way to code

Let's talk about SOLID principles. At first I was afraid of it, I thought it was crazy difficult to use it in real coding but after getting deeper in coding I found my work particularly complex and couple so I needed something to solve the spaghetti that I had.

Enter SOLID, as I was studying it and trying to figure out how to implement it in real life code I began to expand my mind and then it became a second nature to my thinking process so lets go!

1. Single Responsibility Principle (SPR)
This one states that each class or function should have only one responsibility. This means that a class or function should not have too many different tasks, but should instead focus on doing one thing and doing it well. Or in the words of uncle Bob, it should have only one reason to change.

For example, we have to code a feature for a user story that states that the client needs to create an employee and save their data into the database, so, instead of having an "Employee" class that handles both the business logic of an employee and also manages the database connection, we should have an "Employee" class that only handles the business logic of an employee and a separate "Database Connection" class that handles the database connection. This way, if we need to change the way the database connection is made, we won't have to modify the "Employee" class.

All code is Typescript.

class Employee {
  private age: number;
  private name: string;

  constructor(age: string, name: string) {
    this.age = age;
    this.name = name;
  }

  public getAge() {
    // Logic to get age.
  }
}

class DBConnection {
  public saveEmployee(employee: Employee) {
    // Logic to save employee into the DB.
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Open/Close Principle (OCP)
This states that a class should be open for extension but closed for modification. This means that we should be able to add new functionalities to a class without having to modify its original code.

So think of this scenario, there is a new feature that needs to be deployed and it should be done along a code that is already developed, tested, and it is working in production, the last thing you want is to change that code into a mess so you should think ahead of this scenario and code in a way that is scalable an easy to maintain.

For example, if we have a "Vehicle" class that has a "move" method, if we want to add a new type of vehicle like a boat, instead of modifying the "Vehicle" class and adding a new "sail" function, we should create a new "Boat" class that extends the "Vehicle" class and adds the new functionality.

class Vehicle {
  public move() {
    // Logic to move
  }
}

class Boat extends Vehicle {
  public navigate() {
    // Logic to navigate
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Liskov Substitution Principle (LSP)
It is easier than it sounds, It states that an instance of a derived class should be able to be used wherever an instance of the base class is expected without altering the correctness of the program.

In other words, if we have a base class and a derived class that extends the base class, any object of the derived class should be able to be used in place of an object of the base class without causing errors or unexpected behavior.

Please don't use inheritance, use abstract class, your future you will thank you

For example, if we have a base class "Shape" that has a "calculateArea" method and a derived class "Circle" that extends "Shape", then an object of "Circle" should be able to be used in place of an object of "Shape" anywhere in the code where a "Shape" is expected.

abstract class Figure {
  public abstract getArea(): number;
}

class Circle extends Figure {
  private radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  public getArea() {
    return Math.PI * Math.pow(this.radius, 2);
  }

  public getPerimeter() {
    return 2 * Math.PI * this.radius;
  }
}

// Use a circle where a figure is expected
function printArea(figura: Figure) {
  console.log(`The figure area is: ${figure.getArea()}`);
}

const circle = new Circle(5);
printArea(circle); 
/* This wont yield errors since a Circle object 
*can be used in exchange for a Figure object and 
*the program will continue tu run
*
*Try creating another class for any figure and 
*simply change Circle(5) for the created class
*/
Enter fullscreen mode Exit fullscreen mode

4. Interface Segregation Principle (ISP)
Have you ever seen a class that implements a ton of unused methods? Well the ISP states that a class should not be forced to implement methods that it doesn't need. This means that we should have smaller and more specific interfaces instead of one large and complex interface.

In other words, lets think interface first (yep design before coding)

Let's say we are modeling the workers of an IT company, if we have an interface "Worker" that has methods for "work", "rest" and "attend meetings", but a "Developer" class only needs the "work" and "rest" methods, then we should not force the "Developer" class to implement the "attend meetings" method.

So lets think in terms of what a person can do in a company and segregate it into several interfaces:

interface Worker {
  rest(): void;
  work(): void;
}

interface AttendMeeting {
  attendMeeting(): void;
}

class Dev implements Worker {
  public work() {
    // Logic to work as a developer
  }

  public rest() {
    // Logic to rest as a developer
  }
}

class Coordinator implements Worker, AttendMeeting {
  public work() {
    // Logic to work as a coordinator
  }

  public rest() {
    // Logic to rest as a coordinator
  }

  public attendMeeting() {
    // Logic to attend meetings as a coordinator
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP)
Have you ever return to a code you wrote some time ago? Well DIP aims to make code more flexible and easier to maintainable in the future.

If you are making unit testing (and you should), then this principle is pure gold.

The main idea is that high-level modules should not depend on low-level modules, but both should depend on abstractions. This means that we should depend on interfaces and not on concrete implementations.

Lets say that you are tasked with the always fun feature of writing a program that allows users to log in (who am I kidding). If you directly depend on the concrete implementation of the login service, then if in the future if you want to change the way the login works, you will have to change all of your code (yay!).

Instead, if you depend on an abstraction, such as an interface that defines the functionality of the login service, you can easily change the implementation without having to modify the rest of the code.

interface AuthService {
  login(email: string, password: string): void;
}

class FirebaseAuthService implements AuthService {
  constructor() {}

  public login(email: string, password: string) {
    // Logic to login using Firebase Authentication
  }
}

class User {
  constructor(private authService: AuthService) {}

  public login(email: string, password: string) {
    this.authService.login(email, password);
  }
}

const authService = new FirebaseAuthService();
const user = new User(authService);
Enter fullscreen mode Exit fullscreen mode

In the previous code we create a FirebaseAuthService class that implements the interface AuthService and then we inject it into the user through the constructor, which means that we now depend on an abstraction instead of a concrete implementation and we can change that for anything that implements the AuthService interface (Remember LSP).

I hope this explanation helps you get a better grasp of SOLID and get you to learn how to implement them and see that the principles should be treated as one (think in SOLID as one think instead of separate principles).

Let me know what you think in your comments.

Top comments (0)