SOLID Principles

Principles of Object Oriented Desig

SOLID


In the world of software development, creating clean and maintainable code is essential. However, at times, it can be complex and challenging.


At the end of the 20th century, the software development industry was going through a difficult period. Developers were faced with the task of maintaining and improving highly complex systems. As software projects grew, their breadth and depth increased, creating difficulties in managing and maintaining them.


It is in this context where SOLID principles come into play. These five principles, created by software engineer Robert C. Martin, offer guidance for creating high-quality software.


These five principles provide a compass to guide developers in addressing the challenges inherent in creating and maintaining clean, robust code.


What are the SOLID principles?


1 - Single Responsibility:


"A class should have one and only one reason to change, which means a class should have only one job."


Example:

Imagine we are building an employee management system in Node.js. Instead of having a single giant class, we can divide responsibilities into separate modules:


  
  class Employee {
    constructor(name, salary) {
      this.name = name;
      this.salary = salary;
    }
  
    calculateSalary() {
      // Salary-related calculations
    }
  }
  
  class EmployeeRepository {
    saveEmployee(employee) {
      // Logic for saving to the database
    }
  }
  


2 - Open/Closed:


"Objects or entities should be open for extension but closed for modification."


Example:


Suppose we have a Form class with methods to calculate the area of different geometric shapes. Applying the open/closed principle:


  class Form {
    // Methods to calculate the area of different shapes
  }
  
  class Circle extends Form {
    calculateArea() {
      // Calculation of the area of a circle
    }
  }
  
  class Rectangle extends Form {
    calculateArea() {
      // Calculation of the area of a rectangle
    }
  


This way, we can extend the functionality of the Form class without modifying its existing code by adding new subclasses.


3 - Liskov Substitution:


"Every subclass or derived class should be substitutable for its base or parent class."


Example:


Suppose we have a class hierarchy for geometric figures and want to apply the Liskov substitution principle:


  class Figure {
    calculateArea() {
      return 0.0;
    }
  }
  
  class Circle extends Figure {
    calculateArea() {
      // Calculation of the area of a circle
    }
  }
  
  class Square extends Figure {
    calculateArea() {
      // Calculation of the area of a square
    }
  


Any instance of a subclass (e.g., Circle or Square) can be used in place of an instance of the base class Figure without issues.


4 - Interface Segregation:


"One should not be forced to implement an interface they do not use, and clients should not be forced to depend on methods they do not use."


Example:


Let's imagine a Node.js printing system that allows printing documents. Applying the interface segregation principle:


  
  class Printer {
    print() {
      // Implementation of printing
    }
  }
  
  class Scanner {
    scan() {
      // Implementation of scanning
    }
  


This way, classes can implement only the methods they need.


5 - Dependency Inversion:


"The high-level module should not depend on the low-level module, but both should depend on abstractions."


Example:


Suppose we have a Motor class used by a Car class. Applying the dependency inversion principle:


  
  class Motor {
    start() {
      // Logic for starting a motor
    }
  }
  
  class Car {
    constructor(motor) {
      this.motor = motor;
    }
  
    startCar() {
      this.motor.start();
    }
  


This way, the high-level module (Car) depends on an abstraction (Motor) rather than a specific implementation.