# SOLID principles with JavaScript

## SOLID

SOLID stands for 5 of the following principles,

* Single Responsibility Principle
    
* Open-Closed Principle
    
* Liskov Substitution Principle
    
* Interface Segregation Principle
    
* Dependency Inversion Principle
    

### Single Responsibility Principle

The Single Responsibility Principle implies,

* There should be one single reason to change a module
    

When violating the single responsibility principle,

```javascript
// Breaking SRP by combining data and display responsibilities
class BandManager {
  constructor() {
    this.bands = [];
  }

  // Data management responsibilities
  addBand(bandName) {
    this.bands.push(bandName);
  }
  
  removeBand(bandName) {
    this.bands = this.bands.filter(band => band !== bandName);
  }
  
  // Display responsibilities
  displayBands() {
    console.log("Current lineup:");
    this.bands.forEach(band => {
      console.log(`- ${band}`);
    });
  }
  
  displayBandsAsTable() {
    console.log("Band Lineup Table:");
    console.log("------------------");
    this.bands.forEach((band, index) => {
      console.log(`${index + 1}. ${band}`);
    });
    console.log("------------------");
  }
  
  printBandsToFile(filename) {
    console.log(`Saving band list to ${filename}...`);
    // Code to write to file would go here
  }
}

// Usage
const concertLineup = new BandManager();
concertLineup.addBand("Metallica");
concertLineup.addBand("Iron Maiden");
concertLineup.displayBands();
concertLineup.displayBandsAsTable();
```

Here the `BandManager` has two reasons to change,

* Data management
    
* Display management
    

A better approach using single responsibility principles is,

```javascript
// Only responsible for band data management
class BandCollection {
  constructor() {
    this.bands = [];
  }

  addBand(bandName) {
    this.bands.push(bandName);
  }
  
  removeBand(bandName) {
    this.bands = this.bands.filter(band => band !== bandName);
  }
  
  getAllBands() {
    return [...this.bands]; // Return a copy to prevent external modification
  }
}

// Only responsible for displaying band information
class BandDisplay {
  displayAsList(bands) {
    console.log("Current lineup:");
    bands.forEach(band => {
      console.log(`- ${band}`);
    });
  }
  
  displayAsTable(bands) {
    console.log("Band Lineup Table:");
    console.log("------------------");
    bands.forEach((band, index) => {
      console.log(`${index + 1}. ${band}`);
    });
    console.log("------------------");
  }
}

// Only responsible for file operations
class FileService {
  saveBandsToFile(bands, filename) {
    console.log(`Saving band list to ${filename}...`);
    // Code to write to file would go here
  }
}

// Usage
const bandCollection = new BandCollection();
const display = new BandDisplay();
const fileService = new FileService();

bandCollection.addBand("Metallica");
bandCollection.addBand("Iron Maiden");

// Get the data from one class and display it using another
const bands = bandCollection.getAllBands();
display.displayAsList(bands);
display.displayAsTable(bands);
fileService.saveBandsToFile(bands, "concert_lineup.txt");
```

Here we isolate the logging in a separate module. Any time our logging mechanism is being changed, we can simply update the `Logger` class.

### Open Closed Principle

Open closed principle implies that,

* A module should be open for extension but closed for modification
    

Consider the following code, if we update another payment, we need to add another switch case. This modifies the existing class `PaymentProcessor`.

```javascript
class PaymentAdapter {
  makePayment() {
    throw new Error('makePayment() must be implemented');
  }
}

class StripePayment extends PaymentAdapter {
  makePayment() {
    console.log('Payment made using Stripe');
  }
}

class PaypalPayment extends PaymentAdapter {
  makePayment() {
    console.log('Payment made using PayPal');
  }
}

class PaymentProcessor {
  constructor(paymentAdapter) {
    this.paymentAdapter = paymentAdapter;
  }

  pay(type) {
    switch (type) {
      case 'stripe':
        new StripePayment().makePayment();
        break;
      case 'paypal':
        new PaypalPayment().makePayment();
        break;
      default:
        throw new Error('Payment type not supported');
    }
  }
}

new PaymentProcessor().pay('stripe');
new PaymentProcessor().pay('paypal');
```

Instead, we can use the following way, where any time, we need a new payment adapter, we can pass it to the `PaymentProcessor`, with no need to update the class.

```javascript
class PaymentAdapter {
  makePayment() {
    throw new Error('makePayment() must be implemented');
  }
}

class StripePayment extends PaymentAdapter {
  makePayment() {
    console.log('Payment made using Stripe');
  }
}

class PaypalPayment extends PaymentAdapter {
  makePayment() {
    console.log('Payment made using PayPal');
  }
}

class PaymentProcessor {
  constructor(paymentAdapter) {
    this.paymentAdapter = paymentAdapter;
  }

  pay() {
    this.paymentAdapter.makePayment();
  }
}

const stripePayment = new StripePayment();
const paypalPayment = new PaypalPayment();

new PaymentProcessor(stripePayment).pay();
new PaymentProcessor(paypalPayment).pay();
```

---

### Liskov Substitution Principle

Liskov Substitution Principle implies that,

* Objects of the superclass should behave like objects of the subclass, they should be replaceable
    

Consider the following code, which violates the Liskov Substitution Principle.

```typescript
// Base class
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  setWidth(width) {
    this.width = width;
  }
  
  setHeight(height) {
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }
}

// Subclass that violates LSP
class Square extends Rectangle {
  constructor(size) {
    super(size, size);
  }
  
  // LSP VIOLATION: Changes behavior of parent method
  setWidth(width) {
    this.width = width;
    this.height = width; // Also changes height!
  }
  
  // LSP VIOLATION: Changes behavior of parent method
  setHeight(height) {
    this.width = height; // Also changes width!
    this.height = height;
  }
}

// Client code - will break with Square
function testRectangle(rectangle) {
  rectangle.setWidth(5);
  rectangle.setHeight(4);
  
  // Expects area to be 20, but with Square it will be 16!
  if (rectangle.getArea() !== 20) {
    console.log("LSP violation detected!");
  }
}

const rect = new Rectangle(3, 3);
testRectangle(rect); // Works as expected

const square = new Square(3);
testRectangle(square); // Fails - LSP violation!
```

The code violates the principles,

* A square is forced to inherit from a Rectangle despite having different invariants
    
* The Square subclass changes the behavior of the parent methods `setWidth()` and `setHeight()`
    
* When setting width or height, Square modifies both dimensions to maintain its "squareness"
    
* Client code expecting independent width and height modification will break with a Square
    
* The postcondition of the Rectangle methods (that only one dimension changes) is violated by the Square
    

We can fix the following to align with the principles,

* Created a common base class (Shape) with only truly shared behavior
    
* Removed the inheritance relationship between Rectangle and Square
    
* Made both Rectangle and Square implement the Shape interface independently
    
* Gave Square its appropriate method (`setSize()`) that aligns with its invariants
    
* Ensured each class has methods that match its constraints and behavior
    
* Client code now works with each shape according to its actual capabilities
    

The rewritten version aligns with the Liskov Substitution Principle:

```javascript
// Base shape interface (could be a class)
class Shape {
  getArea() {}
}

// Specific implementations
class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  
  setWidth(width) {
    this.width = width;
  }
  
  setHeight(height) {
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(size) {
    super();
    this.size = size;
  }
  
  setSize(size) {
    this.size = size;
  }
  
  getArea() {
    return this.size * this.size;
  }
}

// Client code is now shape-specific
function testRectangle(rectangle) {
  if (!(rectangle instanceof Rectangle)) {
    throw new Error("Rectangle expected");
  }
  
  rectangle.setWidth(5);
  rectangle.setHeight(4);
  console.log("Area:", rectangle.getArea()); // Always 20
}

function testSquare(square) {
  if (!(square instanceof Square)) {
    throw new Error("Square expected");
  }
  
  square.setSize(4);
  console.log("Area:", square.getArea()); // Always 16
}

const rect = new Rectangle(3, 3);
testRectangle(rect);

const square = new Square(3);
testSquare(square);
```

---

### Interface Segregation Principle

The Interface Segregation Principle implies that an interface should not have any properties that are not used or required by the class.

In the following example, we have a `Shape` interface that is implemented in the `Square` class and `Cube` class. We can calculate the volume for a cube, not for a square. So when we implement `Shape` for the `Square` class, it throws an error for the `volume` method.

```javascript
interface Shape {
  area: () => void;
  volume: () => void;
}

class Square implements Shape {
  height: number;
  width: number;
  constructor(height: number) {
    this.height = height;
    this.width = height;
  }

  area () {
    console.log(this.height * this.width);
  }

  volume() {
    throw new Error('Volume can not valculated on 2d shape');
  }
}

class Cube implements Shape {
  height: number;
  width: number;
  length: number;
  constructor(height: number, width: number, length: number) {
    this.height = height;
    this.width = height;
    this.length = length;
  }

  area () {
    console.log(this.height * this.width);
  } 

  volume () {
    console.log(this.height * this.width * this.length);
  } 
}

const square = new Square(5);
square.area();
square.volume();

const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
```

To resolve the issue, we can introduce an extended interface `Shape3D`. This new interface will have the `volume` method and Will be used when creating the `Cube` class.

```javascript
interface Shape {
  area: () => void;
}

interface Shape3D extends Shape {
  volume: () => void;
}

class Square implements Shape {
  height: number;
  width: number;
  constructor(height: number) {
    this.height = height;
    this.width = height;
  }

  area () {
    console.log(this.height * this.width);
  }
}

class Cube implements Shape3D {
  height: number;
  width: number;
  length: number;
  constructor(height: number, width: number, length: number) {
    this.height = height;
    this.width = height;
    this.length = length;
  }

  area () {
    console.log(this.height * this.width);
  } 

  volume () {
    console.log(this.height * this.width * this.length);
  } 
}

const square = new Square(5);
square.area();

const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
```

### Dependency Inversion Principle

The dependency Inversion Principle implies,

* 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
    

In the following code, we are making `CourseService` class according to the `CourseController`. Here the `Courservice` should not depend on `CourseController`, instead, both should depend on the interface.

```typescript
class CourseService {
  get() {
    console.log('All the courses');
  }
}

class CourseController {
  constructor() {
    this.courseService = new courseService(); // direct dependency
  }

  getAllCourse() {
    this.courseService.get();
  }
}

const courseController = new CourseController();
courseController.getAllCourse();
```

To resolve this, we need a service to which the `CourseService` can depend on an abstract.

So,

1. The `CourseController` now does not depend on the `CourseService` directly
    
2. Both the `CourseService` class (By implementing interface) and `CourseController` class (by setting `interface` as type of `CourseService`) depends on the `Service` interface
    

```typescript
interface Service {
  get: () => void;
}

// Low level module depends on the abstraction
class CourseService implements Service {
  get() {
    console.log('All the courses');
  }
}

class CourseController {
  courseService: Service; // High level module depends on the abstraction
  constructor(courseService: Service) {
    this.courseService = courseService; // No direct dependency
  }

  getAllCourse() {
    this.courseService.get();
  }
}

const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();
```

### Resources

* [Clean Code: SOLID - Beau teaches JavaScript](https://www.youtube.com/watch?v=XzdhzyAukMM)
    
* [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript#solid)
    
* [Web Dev, Simplified SOLID Design Principles](https://www.youtube.com/playlist?list=PLZlA0Gpn_vH9kocFX7R7BAe_CvvOCO_p9)
    
* [Web Dev, Single Responsibility Principles](https://www.youtube.com/watch?v=UQqY3_6Epbg)
    
* [Web Dev, Open Close Principles](https://www.youtube.com/watch?v=-ptMtJAdj40)
    
* [Web Dev, Liskov Substitution Principle](https://www.youtube.com/watch?v=dJQMqNOC4Pc)
    
* [Web Dev, Interface Segregation Principle](https://www.youtube.com/watch?v=JVWZR23B_iE)
    
* [Web Dev, Dependcy Inverson Principle](https://www.youtube.com/watch?v=9oHY5TllWaU)
    
* [Alternate of Inheritance, Liskov Substitution Principle Problem](https://www.youtube.com/watch?v=nnwD5Lwwqdo)
    
* Educative.io
