SOLID Principles: It's That Easy! 😱 STANDOUT 🌟 with SOLID Principles! 🧙‍♂️✨
Tags: solid, principles, solidprinciples, javascript
Published: 2024-01-21 12:02:08

#### Hey there, 👋 Awesome Developers! 🚀

Today, let's dive into the basics of SOLID principles. If you're ready to level up your coding game! 👇 Let's roll!

In the fast-changing world of coding, making clean and powerful code is crucial for building strong and flexible apps. join us as we explore SOLID Principles - the secret recipe of crafting code that's easy to maintain and works amazingly well! 🌟✨

## Introduction 🚀

!["Uncle Bob"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4tfachue4meyp1l1d3f.jpg) SOLID, introduced by Robert C. Martin (Uncle Bob), is a set of five design principles for creating a clean and effective object-oriented code. Here we'll break down each SOLID principle and see how they work in the context of coding with javascript.

---

## 1. Single Responsibility principle (SRP) 🎯

!["Single Responsibility"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6q7o9d8r6gen4gzbw6o.gif)

This is one of the SOLID Principles of object-oriented design. simply put, it suggests that a class should have only one reason to change, or in other words, it should have only one responsibility. SRP says, _"Hey, stick to just one job, and do it really well."_


### Why is this more important ? 
1. **Maintainability:** When a class has a single responsibility, it becomes easier to understand, modify, and maintain. if you need to make a change, you know exactly where to look.

2. **Reusability:** Smaller, focused classes are often more reusable in different parts of your application. you can use them like building blocks to assemble different functionalities.

3. **Flexibility:** With a clear and single responsibility, classes become more adaptable to change. if requirements shift, you can modify or extend individual classes without affecting the entire codebase.

### Example

Let's look at the difference between the code before and after applying the Single Responsibility Principle (SRP) clearer: 

#### Before applying SRP: 

```js
class Report {
   constructor(data) {
     this.data = data;
   }
  
   generateReport() {
     console.log(`Generating report for ${this.data}`);
   }

   saveToFile() {
     console.log(`Saving report to file: ${this.data}`);
     // logic for saving report to a file
   }
}

const report = new Report("Sales Data");
report.generateReport();
report.saveToFile();
```

In this version, the `Report` class is doing two things: Generating a report and saving it to a file. It's responsible for both creating the report content and managing file operations.

#### After applying SRP: 

```js
class Report {
  constructor(data) {
    this.data = data;
  }

  generateReport() {
    console.log(`Generating report for ${this.data}`);
  }
}

class ReportSaver {
  saveToFile(report) {
    console.log(`Saving report to file: ${report.data}`);
    // logic for saving report to a file
  }
}

const report = new Report("Sales Data");
report.generateReport();

const reportSaver = new ReportSaver();
reportSaver.saveToFile(report);
```

In the improved version, we've applied SRP. The `Report` class now focuses solely on generating the report, and a new class, `ReportSaver`, takes care of saving the report to a file. Each class has a single responsibility, making the code more modular and easier to understand and maintain. This separation adheres to the Single Responsibility Principle, ensuring that each class has only one reason to change.

---

## 2. Open/Closed Principle (OCP) 🚪

!["Open/Closed"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d51qsyi5lr6fk5warisa.gif)

This Suggests that a class should be open for extension but closed for modifications. In simpler terms, this means you should be able to add a new feature or functionalities to a system without altering the existing code.

If the concept is still unclear,  let me explain it differently.
The Open/Closed Principle (OCP) is like a rule in a programming that says you can add new things in your code without changing the old stuff. imagine your code is like a LEGO set - you can keep adding new pieces without breaking the ones you already snapped together.

### Why is OCP Important ?
1. **Maintainability:** OCP helps make code easier to handle.  When you want to add new things, you don't have to touch the parts that are already working well. This way, there's less chance of making mistakes and messing up the existing code.

2. **Scalability:** When the software grows (upgrades), being able to add new features without messing with the current code becomes vital for its growth. This way, your code can expand and adapt to new requirements.

3. **Reduced Risk:** Modifying the existing code can bring unexpected problems. OCP helps avoid this by keeping any changes in new code, making it easier to test and fix issues.

4. **Team Collaboration:** When multiple developers work on a project, OCP Allows them to add new features independently without interfering with each other's work.

### Example 

Consider a system that calculates the area of shapes.

#### Before applying OCP:

```js
class Circle {
  radius;

  constructor {
    this.radius = radius; 
  }

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

// Adding a new shape violates OCP
class Square {
  side;

  constructor(side) {
    this.side = side;
  }

  calculateArea() {
    return this.side ** 2;
  }
}

// Object instantiation
const circle = new Circle(5);
const square = new Square(4);

circle.calculateArea(); // 78.54
square.calculateArea(); // 16
```

In this case, if you want to add anew shape like a square, you have to go back and changing the existing code. This Breaks the Open/Closed principle.

#### After applying OCP:

Now, Let's make it better with the Open/Closed Principle:

```js
class Shape(){
  calculateArea(){
    throw new Error("This method should be overridden by subclasses")
  }
}

class Circle extends Shape {
  radius;

  constructor(){
    super();
    this.radius = radius;
  }
 
  calculateArea(){
    return Math.PI * this.radius ** 2
  }
}

class Square extends Shape {
  side;

  constructor(side){
    super();
    this.side = side;
  }

  calculateArea(){
    return this.side ** 2;
  }
}

// Object instantiation
const circle = new Circle(5);
const square = new Square(4);

circle.calculateArea(); // 78.54
square.calculateArea(); // 16
```

In this code, `Shape` is the parent class, and `Circle` and `Square` are its subclasses. when you create an instance of `Circle` or `Square`, the `super()` statement is used to call the constructor of the `Shape` class. This is important because it allows the initialization of any properties or setup defined in the `Shape` class before adding the specific properties and behavior in the constructor of the subclass.

In Simpler terms, `super()` ensures that both the parent (`Shape`) and the Child (`Circle` or `Square`) classes get properly initialized. It's a way to extend the behavior of the parent class while keeping everything consistent.

---

## 3. Liskov Substitution Principle (LSP) 🔄

!["Liskov Substitution"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/08i9ft53jbcpr01ts87c.gif)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

In the simpler terms, if a class is subclass of another class, you should be able to use objects of the subclass wherever objects of the superclass are used, without introducing errors.

If you're still not getting it, Let me simplify it further.
Imagine you have a big category of things (a superclass) and some more specific things that belong to that category (subclasses). LSP suggests that you can use the specific things whenever you use the big category things messing up your program.

### Why is this more important ? 
1. **Code Flexibility:** LSP promotes the interchangeability of objects, allowing for flexibility in code design.

2. **Consistent Behavior:** It ensures that subclasses maintain consistent behavior with the superclasses, reducing unexpected surprises.

### Example 

Let's use a straightforward code example. 🦅🐧

#### Before applying LSP:

```js
class Bird {
  fly(){
    console.log("Flying High!");
  }
}

class Penguin extends Bird {
  // Penguins can't fly, violation LSP
  fly(){
    console.log("I can't fly!");
  }
}

// Object Instantiation
const bird = new Bird();
const penguin = new Penguin();

bird.fly(); // "Flying High!"
penguin.fly(); // "I can't fly!"
```

In this, The `Penguin` class violates LSP by not being able to fly, which is expected from its superclass `Bird`.

#### After applying LSP:

```js
class Bird {
  fly() {
    console.log("Flying high!");
  }
}

class Penguin extends Bird {
  // Penguins should override fly appropriately
  swim() {
    console.log("Swimming gracefully!");
  }
}

// Object instantiation
const bird = new Bird();
const penguin = new Penguin();

bird.fly();      // Output: "Flying high!"
penguin.fly();   // Output: "Flying high!"
penguin.swim();  // Output: "Swimming gracefully!"
```

In this, The `Penguin` class adheres to LSP by introducing a new behavior `swim` without changing the expected behavior of flying. Both `Bird` and `Penguin` instances can be used interchangeably where a `Bird` is expected, ensuring consistency.

---

## 4. Interface Segregation Principle (ISP) 🤝

!["Interface Segregation"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2xnlk3ex96v6qzorl1a.gif)

The Interface Segregation Principle suggests that a class should not be forced to implement interfaces it doesn't use. In simpler terms, it's better to have several small, specific interfaces than one large, all-encompassing interface.

If the concept is still unclear, Let me explain it differently,
The Interface Segregation Principle (ISP) is like a rule in coding that says: "Don't make a class do things it doesn't need to do. It's better to have many small and specific sets of tasks (interfaces) for a class rather than one big set that does everything."

It's like telling a chef to focus on their specialty dishes instead of making them handle every type of cuisine. 🍳🍕

### Why is this more Important ?
1. **Flexibility:** It allows for more flexibility in implementing interfaces, preventing unnecessary dependencies on methods that aren't relevant.

2. **Avoiding Bloat:** Classes don't need to implement methods they don't use, keeping the codebase clean and avoiding unnecessary bloat.

In JavaScript, lacking an explicit `interface` keyword, I'm using TypeScript in this example instead of JavaScript. If you want to implement a similar approach in JavaScript, you can rely on implicit interfaces, where classes or objects share a common set of methods.

### Example 
Let's create a Example in Typescript for Implementation of Interface Segregation Principle (ISP).

#### Before applying ISP:

```ts
interface Worker {
  work(): void;
  eat(): void;
}

class Engineer implements Worker {
  work() {
    console.log("Engineer working...");
  }

  eat() {
    console.log("Engineer eating...");
  }
}

class Manager implements Worker {
  work() {
    console.log("Manager working...");
  }

  eat() {
    console.log("Manager eating...");
  }
}

// Object instantiation
const engineer = new Engineer();
const manager = new Manager();

// Example usage
engineer.work();  // Output: "Engineer working..."
engineer.eat();   // Output: "Engineer eating..."

manager.work();   // Output: "Manager working..."
manager.eat();    // Output: "Manager eating..."
```
In this, the `Worker` interface has both work and eat methods, and both `Engineer` and `Manager` are forced to implement both methods.

#### After applying ISP:

```ts
// With ISP
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class Worker implements Workable, Eatable {
  work() {
    console.log("Working...");
  }

  eat() {
    console.log("Eating...");
  }
}

class Engineer implements Workable {
  work() {
    console.log("Engineer working...");
  }
}

class Manager implements Workable, Eatable {
  work() {
    console.log("Manager working...");
  }

  eat() {
    console.log("Manager eating...");
  }
}

// Object instantiation
const worker = new Worker();
const engineer = new Engineer();
const manager = new Manager();

// Example usage
worker.work();    // Output: "Working..."
worker.eat();     // Output: "Eating..."

engineer.work();  // Output: "Engineer working..."

manager.work();   // Output: "Manager working..."
manager.eat();    // Output: "Manager eating..."
```

In this, we split the interface into `Workable` and `Eatable`, allowing classes to implement only the interfaces relevant to their functionality.

---

## 5. Dependency Inversion Principle (DIP) 🔄

!["Dependency Inversion"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/si9qqe4cs06kemfyunx4.gif)

The Dependency Inversion Principle emphasizes high-level modules should not depend on low-level modules but rather both should depend on abstractions. Additionally, it advocates that abstractions should not depend on details; details should depend on abstractions.

If you're still not getting it, Let's simplify it further.

The Dependency Inversion Principle (DIP) is like a rule in coding that says: "Don't have important parts of your code rely too much on each other. Instead, make them both rely on general plans (abstractions). And remember, these general plans shouldn't worry about specific details; the details should take their cues from the general plans."

It's like building with LEGO bricks, where each brick follows a common design, and the specifics of each brick don't bother the overall structure. 🧱🌐

### Why is this more Important ?
1. **Flexibility:** It promotes a flexible and extensible design by decoupling high-level and low-level components.

2. **Easy Maintenance:** Changes in low-level details don't impact high-level policies, making the system easier to maintain.

### Example 
Let's consider a system called Smart Home Control.

#### Before applying DIP:

```js
class LightBulb {
  turnOn() {
    console.log("LightBulb: Turning on...");
  }

  turnOff() {
    console.log("LightBulb: Turning off...");
  }
}

class Switch {
  constructor(bulb) {
    this.bulb = bulb;
  }

  operate() {
    this.bulb.turnOn();
    // Some other operations
    this.bulb.turnOff();
  }
}

// Object instantiation
const bulb = new LightBulb();
const switch = new Switch(bulb);

switch.operate();
```

In this, `Switch` directly depends on `LightBulb` and that violates the Dependency Inversion Principle.

#### After applying DIP:

```js
// Interface-like abstraction
class Switchable {
  turnOn() {
    throw new Error("Method not implemented");
  }

  turnOff() {
    throw new Error("Method not implemented");
  }
}

class LightBulb extends Switchable {
  turnOn() {
    console.log("LightBulb: Turning on...");
  }

  turnOff() {
    console.log("LightBulb: Turning off...");
  }
}

class Switch {
  constructor(device) {
    this.device = device;
  }

  operate() {
    this.device.turnOn();
    // Some other operations
    this.device.turnOff();
  }
}

// Object instantiation
const bulb = new LightBulb();
const switch = new Switch(bulb);

switch.operate();
```
In this, an abstraction (`Switchable`) is used, and both `LightBulb` and `Switch` depend on this abstraction, following the Dependency Inversion Principle. 
_Note that JavaScript doesn't have explicit interfaces, so we use a class as an abstraction here._

---

## Conclusion 🌟
In a nutshell, This post introduces you to the SOLID principles—a set of rules for writing cleaner and more maintainable code. While we've touched on the basics here, there's more to explore for a complete grasp.

---

## That's it 😁
Thanks for diving into this blog 🙏. If you found it helpful, share your thoughts in the comments 📩.

And remember to give a "💖 🦄 🤯 🙌 🔥" if you enjoyed it!

!["Thank you"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wu95uenll5ds79ibiny2.gif)