Skip to content

SOLID Principles

D. Hristov edited this page Sep 19, 2023 · 1 revision

SOLID Principles - Should I know them?

The idea behind the SOLID is to make software designs more understandable, flexible, and maintainable - So simple, right?

Single Responsibility Principle (S)

The point of this principle is to reduce complexity of our class/function by making it responsible only for one thing!

Wrong implementation

class DB {
  doQuery<T>(url: string): Promise<T> {
    // TODO Do some code
    this.doLogs();
  }

  // This class shouldn't be responsible to log anything!
  doLogs(): void {
    //TODO Write who made the query
    //TODO What is retrieved from the query
    ....
  }
}

Correct implementation

class Logger {
  private static instance: Logger;

  private constructor() {}

  static getInstance() {
    if (!Logger.instance) Logger.instance = new Logger();

    return Logger.instance;
  }

  doLogs(): void {
    //TODO Write who made the query
    //TODO What is retrieved from the query
  }
}

class DB {
  private readonly logger: Logger;

  constructor() {
    this.logger = Logger.getInstance();
  }

  doQuery<T>(url: string): Promise<T> {
    // TODO Do some code

    this.logger.doLogs();
  }
}
Code in action
const db = new DB();
db.doQuery<IUser>("select * from users as u where u.uid = 12345");

Open/Closed Principle (O)

The main idea of this principle is to keep existing code from breaking when you implement new features. That means the class/function should be open for extension but closed for modification.

interface ICalculator {
  getArea(shape: Shape): number;
}

class Shape {
  readonly width: number;
  readonly height: number;
  readonly type: SHAPE_TYPES;

  constructor(a: number, b: number, type: SHAPE_TYPES) {
    this.width = a;
    this.height = b;
    this.type = type;
  }
}

Wrong implementation

enum SHAPE_TYPES {
  RECTANGLE = "RECTANGLE",
  SQUARE = "SQUARE",
}

class Calculator implements ICalculator {
  getArea(shape: Shape): number {
    switch (shape.type) {
      case SHAPE_TYPES.RECTANGLE:
        return shape.width * shape.height;
        break;
      case SHAPE_TYPES.SQUARE:
        return shape.width ** 2;
        break;
    }

    return -1;
  }
}
Code in action
const calculator: Calculator = new Calculator();

const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);

calculator.getArea(rectangle);
calculator.getArea(square);

Correct implementation

class RectangleCalculator implements ICalculator {
  getArea(shape: Shape): number {
    return shape.width * shape.height;
  }
}

class SquareCalculator implements ICalculator {
  getArea(shape: Shape): number {
    return shape.width ** 2;
  }
}
Code in action
const rectangleCalculator: RectangleCalculator = new RectangleCalculator();
const squareCalculator: SquareCalculator = new SquareCalculator();

const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);

rectangleCalculator.getArea(rectangle);
squareCalculator.getArea(square);

Liskov Substitution Principle (L)

This principle encourages us to use composition instead of inheritance where is possible.

interface ICalculator {
  getArea(shape: Shape): number;
}

class Calculator implements ICalculator {
  private shape: Shape;

  constructor(shape: Shape) {
    this.shape = shape;
  }

  getArea(): number {
    switch (this.shape.type) {
      case SHAPE_TYPES.RECTANGLE:
        return this.shape.width * this.shape.height;
        break;

      case SHAPE_TYPES.SQUARE:
        return this.shape.width ** 2;
        break;
    }

    return -1;
  }
}
Code in action
const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);

const rectangleCalculator: Calculator = new Calculator(rectangle);
const squareCalculator: Calculator = new Calculator(square);

rectangleCalculator.getArea();
squareCalculator.getArea();

Interface Segregation Principle (I)

Classes shouldn’t be forced to depend on methods they do not use. In other words we are using separation of the interfaces:

Wrong implementation!

interface IVehicle {
  drive(): void;

  fly(): void;
}

class Car implements IVehicle {
  drive(): void {
    // TODO Do some code
  }

  fly(): void {
    // the car still cannot fly!
  }
}

class Plane implements IVehicle {
  drive(): void {
    // the Plane can only  fly!
  }

  fly(): void {
    // TODO Do some code
  }
}

Correct implementation!

interface ICar {
  drive(): void;
}

interface IPlane {
  fly(): void;
}

class Car implements ICar {
  drive(): void {
    // TODO Do some code
  }
}

class Plane implements IPlane {
  fly(): void {
    // TODO Do some code
  }
}
Code in action
const car: Car = new Car();
const plane: Plane = new Plane();

car.drive();
plane.fly();

Dependency Inversion Principle (D)

High-level classes/modules shouldn’t depend on low-level classes/modules. Both should depend on abstractions. Abstractions shouldn’t depend on details. Details should depend on abstractions. In other words using interfaces/abstract classes! Example:

interface ILogistic {
  deliver(): void;
}

class TruckLogistic implements ILogistic {
  deliver(): void {
    // TODO Do some code
  }
}

class ShipLogistic implements ILogistic {
  deliver(): void {
    // TODO Do some code
  }
}
Code in action
const truck: TruckLogistic = new TruckLogistic();
const ship: ShipLogistic = new ShipLogistic();

truck.deliver();
ship.deliver();