SOLID Principles: They're Rock-Solid for Good Reason!
Tags: programming, productivity, career, beginners
Published: 2024-02-02 17:22:31

Just started with **object-oriented programming** and feeling a bit lost about **SOLID**? No worries, in this article, I'll explain it to you and provide examples of how to use it in your code development.

## What is SOLID?

In object-oriented programming, **SOLID** is an acronym for five design principles aimed at enhancing the understanding, development, and maintenance of software.

By applying this set of principles, you should notice a reduction in bugs, improved code quality, the production of more organized code, decreased coupling, enhanced refactoring, and encouragement of code reuse. Let's get to them.

## 1. S - Single Responsibility Principle

![SRP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v564d0p0s36fwo6imofo.png)
> SRP - Single Responsibility Principle

This one is really simple, but super important: **one class should have one, and only one, reason to change.**

No more creating classes with multiple functionalities and responsibilities, huh? You've probably encountered or even created a class that does a bit of everything, a so-called _God Class_. It might seem fine at the moment, but when you need to make changes to the logic of that class, problems are sure to arise.

> God class: In OOP, this is a class that `do` or `knows` too much things.

```typescript
class ProfileManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }

  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }

  setUserPermissions(username: string): void {
    // Set permission logic
  }
}
```

This **ProfileManager** class is violating the SRP principle by performing **FOUR** distinct tasks. It is validating and updating data, doing the presentation, and to top it off, it's setting the permissions, all at the same time.

### Issues this can cause

- `Lack of cohesion -` a class shouldn't take on responsibilities that aren't its own;
- `Too much information in one place -` your class will end up with many dependencies and difficulties for changes;
- `Challenges in implementing automated tests -` it's hard to mock such a class.

Now, applying **SRP** to the `ProfileManager` class, let's see the improvement this principle can bring:

```typescript
class AuthenticationManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }
}

class UserProfileManager {
  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }
}

class PermissionManager {
  setUserPermissions(username: string): void {
    // Set permission logic
  }
}
```

You might be wondering, `can I apply this only to classes?` The answer is: **NOT AT ALL**. You can (and should) apply it to methods and functions as well.

```typescript
// ❌
function processTasks(taskList: Task[]): void {
  taskList.forEach((task) => {
    // Processing logic involving multiple responsibilities
    updateTaskStatus(task);
    displayTaskDetails(task);
    validateTaskCompletion(task);
    verifyTaskExistence(task);
  });
}

// ✅
function updateTaskStatus(task: Task): Task {
  // Logic for updating task status
  return { ...task, completed: true };
}

function displayTaskDetails(task: Task): void {
  // Logic for displaying task details
  console.log(`Task ID: ${task.id}, Description: ${task.description}`);
}

function validateTaskCompletion(task: Task): boolean {
  // Logic for validating task completion
  return task.completed;
}

function verifyTaskExistence(task: Task): boolean {
  // Logic for verifying task existence
  return tasks.some((t) => t.id === task.id);
}
```

Beautiful, elegant, and organized code. This principle is the foundation for the others; by applying it, you should create high-quality, readable, and maintainable code.

## 2. O - Open-Closed Principle

![OCP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/epfur4p9r55iwbk9i4yy.png)
> OCP - Open-Closed Principle

**Objects or entities should be open for extension but closed for modification.** If you need to add functionality, it's better to extend rather than modify your source code.

Imagine that you need a class to calculate the area of some polygons.

```typescript
class Circle {
  radius: number;

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

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

class Square {
  sideLength: number;

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

  calculateArea(): number {
    return this.sideLength ** 2;
  }
}

class areaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      if (shape instanceof Square) {
        total += (shape as any).calculateArea();
      } else {
        total += shape.area();
      }
    });

    return total;
  }
}
```

The `areaCalculator` class is tasked with calculating the area of diferent polygons, each having its own area logic. If you, 'lil dev, needed to add new shapes, like triangles or rectangles, you'd find yourself **altering** this class to make the changes, right? That's where you run into a problem, violating the `Open-Closed Principle`.

What solution comes to mind? Probably adding another method to the class and done, problem solved 🤩. Not quite, young Padawan 😓, that's the problem!

**Modifying an existing class to add new behavior carries a serious risk of introducing bugs into something that was already working.**

> Remember: OCP insists that a class should be closed for modification and open for extension.

See the beauty that comes with refactoring the code:

```typescript
interface Shape {
  area(): number;
}

class Circle implements Shape {
  radius: number;

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

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

class Square implements Shape {
  sideLength: number;

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

  area(): number {
    return this.sideLength ** 2;
  }
}

class AreaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      total += shape.area();
    });

    return total;
  }
}
```

See the `AreaCalculator` class: it no longer needs to know which methods to call to register the class. It can correctly call the area method by calling the contract imposed by the interface, and that's the only thing it needs.

> As long as they implement the Shape interface, all runs fine.

<br/ >
> Separate extensible behavior behind an interface and invert dependencies.
>
> [Uncle Bob](https://en.wikipedia.org/wiki/Robert_C._Martin)

- `Open for extension:` You can add new functionality or behavior to the class without changing its source code.
- `Closed for modification:` If your class already has a functionality or behavior that works fine, don't change its source code to add something new.

## 3. L - Liskov Substitution Principle

![LSP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62uow23fsa5zk2wz5fz5.png)
> LSP - Liskov Substitution Principle

The Liskov Substitution Principle says that **a derived class must be substitutable for its base class.**

This principle, introduced by Barbara Liskov in 1987, can be a bit complicated to understand by reading her explanation. Still, no worries, I'll provide another explanation and an example to help you understand.

> If for each object o1 of type S there is an object o2 of type T such that, for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.
>
> Barbara Liskov, 1987

You got it, right? Nah, probably not. Yeah, I didn't understand it the first time I read it (nor the next hundred times), but hold on, there's another explanation:

> If S is a subtype of T, then objects of type T in a program can be replaced by objects of type S without altering the properties of this program.
>
> [Wikipedia](https://en.wikipedia.org/wiki/Liskov_substitution_principle)

If you're more of a visual learner, don't worry, here's an example:

```typescript
class Person {
  speakName() {
    return "I am a person!";
  }
}

class Child extends Person {
  speakName() {
    return "I am a child!";
  }
}

const person = new Person();
const child = new Child();

function printName(message: string) {
  console.log(message);
}

printName(person.speakName()); // I am a person!
printName(child.speakName()); // I am a child!
```

The parent class and the derived class are passed as parameters, and the code continues to work as expected. Magic? Yeah, it's the magic of our friend Barb.

### Examples of violations:

- Overriding/implementing a method that does nothing;
- Returning values of different types from the base class.
- Throwing an unexpected exception;

## 4. I - Interface Segregation Principle

![ISP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qbdmugukrcacuqthho5x.png)
> ISP - Interface Segregation Principle

This one says that **a class should not be forced to implement interfaces and methods it does not use.** It's better to create more specific interfaces than a big and generic one.

In the following example, an **Book** interface is created to abstract book behaviors, and then classes implement this interface:

```typescript
interface Book {
  read(): void;
  download(): void;
}

class OnlineBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // This implementation doesn't make sense for a book
    // it violates the Interface Segregation Principle
  }
}
```

The generic `Book` interface is forcing the `PhysicalBook` class to have a behavior that makes no sense (_or are we in the Matrix to download physical books?_) and violates both the **ISP** and **LSP** principles.

Solving this problem using **ISP**:

```typescript
interface Readable {
  read(): void;
}

interface Downloadable {
  download(): void;
}

class OnlineBook implements Readable, Downloadable {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Readable {
  read(): void {
    // does something
  }
}
```

Now it's waaay better. We removed the `download()` method from the `Book` interface and added it to a derived interface, `Downloadable`. This way, the behavior is isolated correctly within our context, and we still respect the **Interface Segregation Principle**.

## 5. D - Dependency Inversion Principle

![DIP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/42aomc36xq804pyldlis.png)
> DIP - Dependency Inversion Principle

This one goes like this: **Depend on abstractions and not on implementations.**

> High-level modules should not depend on low-level modules. Both should depend on abstractions.
>
> Abstractions should not depend on details. Details should depend on abstractions.
>
> Uncle Bob

Now I'll show a simple code to illustrate DIP. In this example, there's a service that gets the user from the database. First, let's create a concrete class that connects with the database:

```typescript
// Low-level module
class MySQLDatabase {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}
```

Now, let's create a service class that depends on the concrete implementation:

```typescript
// High-level module
class UserService {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}
```

In the above example, `UserService` directly depends on the concrete implementation of `MySQLDatabase`. This violates **DIP** since the **high-level** class UserService is directly dependent on a **low-level** class.

If we want to switch to a different database system (e.g., PostgreSQL), we need to modify the UserService class, which is `AWFUL`!

Let's fix this code using **DIP**. Instead of depending on concrete implementations, the high-level class `UserService` should depend on abstractions. Let's create a `Database` interface as an abstraction:

```typescript
// Abstract interface (abstraction) for the low-level module
interface Database {
  getUserData(id: number): string;
}
```

Now, the concrete implementations `MySQLDatabase` and `PostgreSQLDatabase` should implement this interface:

```typescript
class MySQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}

// Another low-level module implementing the Database interface
class PostgreSQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from PostgreSQL database
  }
}
```

Finally, the UserService class can depend on the `Database` abstraction:

```typescript
class UserService {
  private database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}
```

This way, the `UserService` class depends on the `Database` abstraction, not on concrete implementations, fulfilling the **Dependency Inversion Principle**.

## Conclusion

By adopting these principles, developers can create systems more resilient to changes, making maintenance easier and improving code quality over time.

The entirety of this article is derived from various other articles, my personal notes, and dozens of online videos that I came across while **delving into the realms of Object-Oriented Programming (OOP)** 🤣. The code snippets utilized in the examples were created based on my interpretation and comprehension of these principles. I really wish, my lil padawan, that I have contributed to advance your understanding and progress in your studies.

I really hope you liked this article, and don't forget to follow!

Note: Images taken from [this article](https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898)