SOLID principles Using JavaScript

SOLID principles Using JavaScript

Explain solid principles using JavaScript and Typescript examples.

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,

class ConcertLineups {
  constructor(maximumBandLimits) {
    this.lineups = [];
  }

  addBandsToLineup(bandName) {
    this.lineups.push(bandName);
  }

  displayLineups() {
    console.log(this.lineups);
  }
}

const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();

Here the ConcertLineups has two reasons to change,

  • How we are adding the lineups
  • How we display the lineups

Now we are just console the lineups. Later there can be a requirement, we want to persist the log or send the log to an analytics service.

A better approach using single responsibility principles is,

class Logger {
  log(message) {
      console.log(message);
  }
}

class ConcertLineups {
  constructor(maximumBandLimits) {
    this.lineups = [];
    this.logger = new Logger();
  }

  addBandsToLineup(bandName) {
    this.lineups.push(bandName);
  }

  displayLineups() {
    this.logger.log(this.lineups);
  }
}

const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();

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.

class StripePayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
}

class PaypalPayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
}

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

  pay() {
    switch(this.paymentAdaptar.paymentType) {
      case 'Stripe':
        makeStripePayment();
        break;
      case 'Paypal':
        makePaypalPayment();
        break;
    }
  }
}

const makeStripePayment = () => {
  // make payment
}

const makePaypalPayment = () => {
  // make payment
}

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.

class StripePayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }

  makePayment () {
    // pay with stripe
  }
}

class PaypalPayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }

  makePayment() {
    // pay with paypal
  }
}

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

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

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 example, we have a superclass, Vehicle. We created two sub classes Car and Cycle from the Vehicle. The Vehicle has a method startEngine. But it appears the Cycle does not have an engine.

So the object of Vehicle and the object of Cycle does not behave identically.

class Vehicle {
  constructor(name) {
    this.name = name;
  }

  startEngine() {
    console.log(`${this.name} engine started`);
  }
}

class Car extends Vehicle {
  constructor(name) {
    super(name);
  }
}

class Cycle extends Vehicle {
  constructor(name) {
    super(name);
  }

  startEngine() {
    throw new Error(`${this.name} does not have an engine.`)
  }
}

const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startEngine();

To resolve this issue, we can create two more subclasses after Vehicle, one MotorVehicle that has an engine and another ManualVehicle that does not have an engine.

class Vehicle {
  constructor(name) {
    this.name = name;
  }
}

class MotorVehicle {
  constructor(name) {
    this.name = name;
  }

  startEngine() {
    console.log(`${this.name} engine started`);
  }
}

class ManualVehicle {
  constructor(name) {
    this.name = name;
  }

  startMoving() {
    console.log(`${this.name} started moving`);
  }
}

class Car extends MotorVehicle {
  constructor(name) {
    super(name);
  }
}

class Cycle extends ManualVehicle {
  constructor(name) {
    super(name);
  }

  startEngine() {
    throw new Error(`${this.name} does not have an engine.`)
  }
}

const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startMoving();

Interface Segregation Principle

The interface Segregation Principle implies an interface should not have any property that is 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.

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 use when creating the Cube class.

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.

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

class CourseController {
  constructor(courseService) {
    this.courseService = courseService;
  }

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

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

To resolve this, need a service to which the CourseService can depend on,

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

class CourseService implements Service {
  get() {
    console.log('All the courses');
  }
}

class CourseController {
  courseService: Service;
  constructor(courseService: Service) {
    this.courseService = courseService;
  }

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

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

Resources