Explanation of SOLID in OOP
Tags: programming, javascript, typescript, beginners
Published: 2024-07-05 03:00:00

Introduction
------------

**SOLID** is a set of five fundamental principles that support enhancing maintainability and ease of extension for future software development. Introduced by software engineer Robert C. Martin, also known as "Uncle Bob," in the book "Design Principles and Design Patterns," the SOLID principles include:

*   S - Single Responsibility Principle
*   O - Open/Closed Principle
*   L - Liskov Substitution Principle
*   I - Interface Segregation Principle
*   D - Dependency Inversion Principle

Below, we'll provide detailed explanations and analysis for each principle.

Note that the examples in this article are implemented using **TypeScript**, but you can rewrite them in other **object-oriented programming languages**.

![SOLID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wvg02725p8mh8dijnuh.png)


1\. Single Responsibility Principle (SRP)
-----------------------------------------

**_A class should have one and only one reason to change, meaning that a class should have only one job._**

This is considered the simplest and most crucial principle because it relates to most of the other principles. Simply put, when implementing a class/method, it should serve only one specific task. If it has more than one responsibility, it's advisable to split those responsibilities into multiple classes or methods. This practice benefits future maintenance, as when there's a need to modify the functionality of a class/method, we only need to make changes within that class/method without affecting others in the application.

Below is an example illustrating a violation of this principle.

```ts
class Car {
  name: string
  brand: string

  // format car info
  getCarInfoText() {
    return "Name: " + this.name + ". Brand: " + this.brand;
  }
  getCarInfoHTML() {
    return "<span>" + this.name + " " + this.brand + "</span>";
  }
  getCarInfoJson() {
    return { name: this.name, brand: this.brand }
  }

  // store data
  saveToDatabase() { }
  saveToFile() { }
}
```

Here's an example showing a violation of the **SRP** principle because the **Car** class has too many unrelated functions like formatting info and storing data. These functions should be split into different classes to make things clearer and reduce the complexity of the source code.

```ts
// only car info
class Car {
  name: string
  brand: string
}

// only use for format
class Formatter {
  formatCarText(car: Car) {
    return 'Name: ' + car.name + '. Brand: ' + car.brand
  }
  formatCarHtml(car: Car) {
    return '<span>' + car.name + ' ' + car.brand + '</span>'
  }
  formatCarJson(car: Car) {
    return {name: car.name, brand: car.brand}
  }
}

// only use for store data
class Store {
  saveToDatabase() {}
  saveToFile() {}
}
```

2\. Open/Closed Principle (OCP)
-------------------------------

**_Objects or entities should be open for extension but closed for modification_**

This principle means that a class should be designed in a way that allows new functionality to be added without altering its existing code. To achieve this, we can utilize inheritance, interfaces, or composition.

```ts
interface Shape {
    calculateArea(): number;
}

class Circle implements Shape {
    private radius: number;

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

    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle implements Shape {
    private width: number;
    private height: number;

    constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
    }

    calculateArea(): number {
        return this.width * this.height;
    }
}
```

In the example above, the **Shape** interface defines a method called **_calculateArea_** used to calculate the area of a shape. Both the **Circle** and **Rectangle** classes, when implementing this interface, must define their own way of calculating the area for each shape.

This implementation approach is beneficial for future extension. If we add more shapes in the future (such as triangles, quadrilaterals, etc.), we only need to implement the **Shape** interface similarly and define the method to calculate the area for each shape, without needing to modify the existing classes. This avoids disrupting the existing logic of the system.

  

3\. Liskov Substitution Principle (LSP)
---------------------------------------

This principle was proposed by Barbara Liskov in 1987.

Its essence is as follows:

**_Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T._**

  

In simpler terms for object-oriented programming, the principle is understood as:

**_Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program._**

  

Below is an example illustrating a violation of **LSP**. In reality, we know that a square is a type of rectangle with equal width and height. However, when implementing methods in the **Square** class that violate the behavior of the **Rectangle** class, it means that **LSP** is being violated.

```ts
class Rectangle {
  height: number
  width: number

  setHeight(height: number) {
    this.height = height
  }

  setWidth(width: number) {
    this.width = width
  }

  calculateArea() {
    return this.height * this.width
  }
}

class Square extends Rectangle {
  // change behavior of super class
  setHeight(height: number) {
    this.height = height
    this.width = height
  }

  // change behavior of super class
  setWidth(width: number) {
    this.height = width
    this.width = width
  }
}

const rect = new Rectangle()
rect.setHeight(10)
rect.setWidth(5)
console.log(rect.calculateArea()) // 5 * 10

const rect1 = new Square()
rect1.setHeight(10)
rect1.setWidth(5)
console.log(rect1.calculateArea()) // result correct but break LSP
```

In this example, when the **Square** class implements the **_setWidth_** and **_setHeight_** methods from the **Rectangle** class, it violates the **LSP** because it changes both the width and height to be equal.

To ensure that the program doesn't violate the **LSP**, it's better to create a parent class, such as the **Shape** class, and then have both **Square** and **Rectangle** inherit from that class.

  

### Additional Note

This principle is highly abstract and prone to violation if you don't fully understand the concept. In **object-oriented programming**, we often build classes based on real-life concepts and objects, such as "a square is a type of rectangle" or "a penguin is a bird." However, you can't directly translate these relationships into source code. Remember, "**_In real life, A is B (a square is a rectangle), but it doesn't necessarily mean that class A should inherit from class B. Class A should only inherit from class B if class A can substitute for class B._**"

  

4\. Interface Segregation Principle (ISP)
-----------------------------------------

**_A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use._**

The ISP encourages breaking down interfaces into smaller parts so that classes don't have to implement unrelated methods. This helps reduce dependency on unnecessary methods and makes the source code more flexible, easier to extend, and maintain.

Here's an example of a violation of the ISP:

```ts
interface Animal {
  eat(): void
  swim(): void
  fly(): void
}

class Fish implements Animal {
  eat() {}
  swim() {}
  fly() {
    throw new Error('Fish can not fly')
  }
}

class Bird implements Animal {
  eat() {}
  swim() {
    throw new Error('Bird can not swim')
  }
  fly() {}
}
```

Because the **Animal** interface has many methods, and some methods may not be applicable to certain species of animals. When the **Fish** and **Bird** classes implement the **Animal** interface, they have to implement all methods, including unnecessary ones. This leads to wasted effort and increases the complexity of the program unnecessarily.

  

The solution is to split the **Animal** interface into smaller interfaces as follows:

```ts
interface Animal {
  eat(): void
}
interface Bird {
  fly(): void
}
interface Fish {
  swim(): void
}

class Dog implements Animal {
  eat() {}
}

class Sparrow implements Animal, Bird {
  eat() {}
  fly() {}
}

class Swan implements Animal, Bird {
  eat() {}
  swim() {}
  fly() {}
}
```

5\. Dependency Inversion Principle (DIP)
----------------------------------------

**_Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions_**

In simpler terms:

*   High-level modules should not rely on low-level modules; both should rely on abstractions.
*   Abstractions should not depend on details; details should depend on abstractions.

  

Here's an example of creating a **DataExporter** class that allows exporting data based on the provided `Exporter` (either **ExcelExporter** or **PdfExporter**). The `export` method is defined in the **Exporter** interface, making it easy to create additional exporters for use without needing to change the current source code.

```ts
interface Exporter {
  export(data): void
}

class ExcelExporter implements Exporter {
  export(data): void {
    console.log('Export excel', data)
  }
}

class PdfExporter implements Exporter {
  export(data): void {
    console.log('Export csv', data)
  }
}

class DataExporter {
  private exporter: Exporter

  constructor(exporter: Exporter) {
    this.exporter = exporter
  }

  async export(): Promise<void> {
    const data = await this.fetchData()
    this.exporter.export(data)
  }

  private async fetchData() {
    return 'Faked data'
  }
}

const excelExporter = new ExcelExporter()
const pdfExporter = new PdfExporter()

const dataExporterExcel = new DataExporter(excelExporter)
dataExporterExcel.export()

const dataExporterPdf = new DataExporter(pdfExporter)
dataExporterPdf.export()
```

It's important to note that the **_Dependency Inversion Principle_** differs from **_Dependency Injection_** because **_Dependency Inversion_** is a principle, while **_Dependency Injection_** is a design pattern. **_Dependency Injection_** is just one of the ways to implement **_Dependency Inversion_**.
  

Conclusion
----------

The 5 **SOLID** principles can be implemented in most object-oriented programming languages like Java, C#, TypeScript, JavaScript, Python, etc. **SOLID** provides a foundational framework that helps developers build source code that is easy to understand, flexible, easily extendable, enhances system maintainability, and minimizes the risk of issues.

**_If you have any suggestions or questions regarding the content of the article, please don't hesitate to leave a comment below!_**

**_If you found this content helpful, please visit [the original article on my blog](https://howtodevez.blogspot.com/2024/04/explanation-of-solid-in-oop.html) to support the author and explore more interesting content._**

<a href="https://howtodevez.blogspot.com/2024/03/sitemap.html" target="_blank" rel="noreferrer"><img src="https://img.shields.io/badge/Blogger-FF5722?style=for-the-badge&logo=blogger&logoColor=white" width="36" height="36" alt="Blogspot" /></a><a href="https://dev.to/chauhoangminhnguyen" target="_blank" rel="noreferrer"><img src="https://img.shields.io/badge/dev.to-0A0A0A?style=for-the-badge&logo=dev.to&logoColor=white" width="36" height="36" alt="Dev.to" /></a><a href="https://www.facebook.com/profile.php?id=61557154776384" target="_blank" rel="noreferrer"><img src="https://img.shields.io/badge/Facebook-1877F2?style=for-the-badge&logo=facebook&logoColor=white" width="36" height="36" alt="Facebook" /></a><a href="https://x.com/DavidNguyenSE" target="_blank" rel="noreferrer"><img src="https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white" width="36" height="36" alt="X" /></a>