SOLID Software engineering principle

Hello guys, Today I'm going to explain SOLID software engineering principle with TypeScript sample codes

SOLID is an acronym that stands for the five principles of object-oriented design. These principles were first introduced by Robert C. Martin in his book "Agile Software Development, Principles, Patterns, and Practices." The SOLID principles are:

  1. S - Single Responsibility Principle

  2. O - Open-Closed Principle

  3. L - Liskov Substitution Principle

  4. I - Interface Segregation Principle

  5. D - Dependency Inversion Principle

Single Responsibility Principle

The basic meaning of this principle is a class should have only one reason to change. or the class should have only one responsibility.

class User {
  constructor(private readonly name: string, private readonly email: string) {}

  public getName(): string {
    return this.name;
  }

  public getEmail(): string {
    return this.email;
  }
}

class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  public createUser(name: string, email: string): void {
    const user = new User(name, email);
    this.userRepository.save(user);
  }
}

In this example, the User class has a single responsibility: to represent a user with a name and an email. The UserService class has a single responsibility: to create and save a new user using the UserRepository.

Open-Closed Principle

According to this principle, every software entity ( classes, modules, functions, ...) should be open for extinction but closed for modifications, because if someone modify the entity that can be break the application it is a risk

interface Shape {
  area(): number;
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  public area(): number {
    return this.width * this.height;
  }
}

class Circle implements Shape {
  constructor(private radius: number) {}

  public area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class AreaCalculator {
  constructor(private shapes: Shape[]) {}

  public sum(): number {
    return this.shapes.reduce((acc, shape) => acc + shape.area(), 0);
  }
}

In this example, the Shape interface is open for extension (you can implement it to create new shapes) but closed for modification (you cannot modify the area method). The AreaCalculator class is also closed for modification since it uses the area method of the Shape interface without needing to know the specific implementation.

Liskov Substitution Principle

The basic idea of this is subtypes must be substitutable for their base types.

class Rectangle {
  constructor(private width: number, private height: number) {}

  public setWidth(width: number): void {
    this.width = width;
  }

  public setHeight(height: number): void {
    this.height = height;
  }

  public getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }

  public setWidth(width: number): void {
    this.width = width;
    this.height = width;
  }

  public setHeight(height: number): void {
    this.width = height;
    this.height = height;
  }
}

In this example, the Square class extends the Rectangle class, but it violates the Liskov Substitution Principle because it overrides the setWidth and setHeight methods in a way that changes the behavior of the getArea method. A client that uses the Rectangle class might expect the getArea method to always return the correct result, but this is not guaranteed when using a Square object.

Interface Segregation Principle

The Interface Segregation Principle (ISP) is a principle of object-oriented design that states that clients (classes or components that use other classes or components) should not be forced to depend on interfaces they do not use.

In other words, a client should not be required to implement methods or properties that it does not need. This can lead to "fat" interfaces that are unnecessarily complex and hard to implement.

Here is an example of how the Interface Segregation Principle can be applied in TypeScript:

interface Animal {
  eat(): void;
  sleep(): void;
}

interface Feline extends Animal {
  meow(): void;
}

interface Canine extends Animal {
  bark(): void;
}

class Cat implements Feline {
  public eat(): void {
    console.log('Eating...');
  }

  public sleep(): void {
    console.log('Sleeping...');
  }

  public meow(): void {
    console.log('Meowing...');
  }
}

class Dog implements Canine {
  public eat(): void {
    console.log('Eating...');
  }

  public sleep(): void {
    console.log('Sleeping...');
  }

  public bark(): void {
    console.log('Barking...');
  }
}

In this example, the Feline and Canine interfaces are more specific versions of the Animal interface that only includes the methods that are relevant to felines and canines, respectively. This allows the Cat and Dog classes to implement only the methods that they need, without being forced to implement unnecessary methods.

Dependency Inversion Principle

Dependency inversion is a principle that states that high-level modules (components or classes that provide complex functionality) should not depend on low-level modules (components or classes that provide simple, underlying functionality). Instead, both should depend on abstractions.

This principle is often referred to as the "Dependency Inversion Principle" (DIP) and is one of the SOLID principles of object-oriented design. It is meant to decouple the design of a software system, making it more flexible and maintainable.

interface Engine {
  start(): void;
  stop(): void;
}

class Car {
  constructor(private readonly engine: Engine) {}

  public start(): void {
    this.engine.start();
  }

  public stop(): void {
    this.engine.stop();
  }
}

class GasEngine implements Engine {
  public start(): void {
    console.log('Starting gas engine...');
  }

  public stop(): void {
    console.log('Stopping gas engine...');
  }
}

class ElectricEngine implements Engine {
  public start(): void {
    console.log('Starting electric engine...');
  }

  public stop(): void {
    console.log('Stopping electric engine...');
  }
}

In this example, the Car class depends on the Engine interface and does not care about the specific implementation of the engine. This allows the Car class to be flexible and easily extensible, as it can work with any type of engine as long as it implements the Engine interface.

These principles are meant to guide the design of software systems in a way that makes them more flexible, maintainable, and scalable. Adhering to the SOLID principles can help you create a codebase that is easier to understand, modify, and test.

One extra kiss from me,

KISS

KISS is an acronym that stands for "Keep It Simple, Stupid." It is a design principle that suggests that systems should be designed to be as simple as possible and that unnecessary complexity should be avoided. The idea behind the KISS principle is that simple systems are easier to use, understand, and maintain than complex ones. Some common applications of the KISS principle include software design, engineering, and project management.