My understanding of SOLID principles
Published: 2021-08-16 16:24:50

SOLID is made up of 5 design principles which is supposed to avoid unnecessary complexity. That being said, some things are unavoidably complex.

## S: Single Responsibility Principle

A class should have a single responsibility

This was slightly confusing for me because I'm wondering how to define the scope of responsibility. We can understand it as the class needing to manage one well-defined aspect of the application.

Say you have an employee at a hotel that wears many hats - Chef, Receptionist, and Cleaner. If we were to create a class Employee which encompasses all of these roles, the responsibility wouldn't be very well defined.

If we need to change how the food is cooked, the class has to be modified. If we need to change how the room is clean, the class has to be modified.

```
class Employee {
  cookFood() {}
  serveGuest() {}
  cleanRoom() {}
}
```

Instead, we can define 3 classes relating to the food (chef), guests (receptionist), and housekeeping (cleaner).

```
class Chef extends Employee {
  cookFood() {}
}

class Receptionist extends Employee {
  serveGuest() {}
}

class Cleaner extends Employee {
  cleanRoom() {}
}
```

## O: Open/Closed Principle

Classes should be open for extension, but closed for modification.

In this context, extension means to extend the class by creating a new sub-class, modification means to modify the existing class.

If we wanted to introduce a Station Chef who's in charge of cooking the omelette at the breakfast buffet line, we could either extend or modify.

This is how it would look like if we were to modify.

```
class Chef extends Employee {
  cookFood() {}
  cookOmelette() {}
}
```

The problem with doing so would be potentially introducing bugs to the Chef class. Other parts of the application that use the Chef class might also fail.

Instead, we can extend, creating a new sub-class StationChef which inherits Chef.

```
class Chef extends Employee {
  cookFood() {}
  cookOmelette() {}
}

class StationChef extends Chef {
  cookOmelette() {}
}
```

## L: Liskov Substitution

The child class needs to be able to perform the same actions as the parent class.

If StationChef extends Chef, objects of Chef can be replaced with objects of Chef.

Can't think of an example using this chef analogy, but a good one I found [here](https://stackoverflow.com/a/584732) is on how a square should not be a subclass of a rectangle.


## I: Interface Segregation

A class should not need to implement methods it does not need.

Say we want to give some chefs the ability to clean, and we do so by adding this method into the Chef interface. Later on, we have a HeadChef, which we don't expect to clean.

However, we are forced to implement the method cleanKitchen for the head chef.

```
interface Chef {
  cookFood(): void;
  cleanKitchen(): void;
}

class HeadChef implements Chef {
  cookFood() {}
  cleanKitchen() {}
}
```

Instead, we could break the interface down, and avoid having to implement the cleanKitchen method in HeadChef.

```
interface ChefCook {
  cookFood(): void;
}

interface ChefClean {
  cleanKitchen(): void;
}

class HeadChef implements ChefCook {
  cookFood() {}
}
```

## D: Dependency Inversion 

High level modules should not depend on low level modules. Both should depend on abstraction.

Abstractions should not depend on details. Details should depend on abstractions.

I have no idea what the second line meant.

Say we want the Chef to log every item that was cooked using the logger.

```
class Chef {
  private logger;
  constructor() {
    this.logger = new Logger();
  }

  cookFood(foodName) {
    this.logger.log(foodName);
  }
}
```

In the above example, our high level module is Chef, and low level module is Logger.

I can clearly see that Chef and Logger are now tightly coupled. Scenario: I want to add a new feature to Logger that is used in the Chef class. If I decide to follow the Open/Closed principle, I would add a subclass, say ChildLogger. This means that my Chef class will also need to change to use the new ChildLogger, which also means that the Chef class (high level module) is dependant on the Logger class (low level module).

However, if I had used dependency inversion, and followed the Liskov principle, I will no longer need to change the Chef class, because a ChildLogger is a type of Logger.

```
class Chef {
  private logger;
  constructor(logger: Logger) {
    this.logger = logger;
  }

  cookFood(foodName) {
    this.logger.log(foodName);
  }
}
```

What about the abstractions and details part? Note that the above example does not violate this rule, but I will continue explaining using this analogy.

The abstraction here is that my logger will record the foodName passed to it. The details will be how this foodName will be stored. It could be sending a SMS to the CEO, or just being recorded in a computer.

When we say that abstraction should not depend on details, we always expect the logger to log the foodName, no matter what the implementation of logger.log is.

When we say that details should depend on abstractions, the very name of the method, log, implies that the data is being logged somewhere. In that case, we should not expect the method, log, to perform any other unrelated operations, such as calling another chef to cook the food.