Dev Conventions
  • Dev Conventions
  • Git
    • Branch Flow
    • Conventional Commits
    • Pull requests template
    • Before making the PRs in typescript, keep in mind
  • Typescript
    • Introduction
    • Good practices
    • Solid
    • Using pnpm for Package Management
    • NestJs
    • NextJs
    • React
    • NestJs Testing
    • React Testing
    • Npm Registry
  • PYTHON
    • Introduction
    • Good practices
    • Testing
    • Naming Convention
  • DevOps
    • Introduction
    • Github Actions
    • Creating a Github Actions Workflow
  • Agile
    • Story Points
Powered by GitBook
On this page
  • What is Cohesion?
  • What is Coupling?
  • How to Apply SOLID Principles in JavaScript
  • The Five SOLID Principles Are:
  1. Typescript

Solid

PreviousGood practicesNextUsing pnpm for Package Management

Last updated 11 months ago


💡 SOLID is an acronym introduced by in the early 2000s, representing the five principles you should consider in object-oriented programming. These principles are merely guidelines you can choose to apply in software development, but they allow you to create extensible, flexible, readable systems with clean code (spoiler: we will talk about clean code in future entries). We can conclude that the SOLID principles allow us a high degree of cohesion and low coupling.

What is Cohesion?

Cohesion in terms of computing refers to the degree to which different elements of the same system remain united, generating a larger element. We could see it as a class that integrates several methods, each of which is related to each other, having a common “theme.”

What is Coupling?

Coupling is the degree to which all these elements are related to each other. The greater the relationships or dependencies, the higher the degree of coupling.

How to Apply SOLID Principles in JavaScript

We have seen a bit of theory, and now we will focus on practice. In this part of the article, we will look at how to apply each of the principles in this wonderful language.

By the way, if you are looking to become a better software developer, check out .

The Five SOLID Principles Are:

  • S – Single Responsibility Principle

  • O – Open/Closed Principle

  • L – Liskov Substitution Principle

  • I – Interface Segregation Principle

  • D - Dependency Inversion Principle

Single Responsibility Principle

It tells us that a class or function should focus on a single responsibility, that there should be a single reason to change; in summary, we can say that this principle asks us that all methods or sub-functions have high cohesion.

class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  getMake() {
    return this.make;
  }

  getModel() {
    return this.model;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }
}

In this example, we can see how the Car class has specific methods for reading and writing information, but it does not do anything additional like saving to a database or calling other unrelated functions.

Open/Closed Principle

It tells us that we should be able to extend the behavior of a class/function without modifying it.

class PantryProducts {
  products = ["Pineapple", "Apples", "Flour"];

  hasProduct(product) {
    // indexOf returns the position of the product in the array,
    // if the position is -1, it means the product does not exist
    return this.products.indexOf(product) !== -1;
  }
}

If we wanted to add the ability to add more products to the PantryProducts class, we would do the following:

class PantryProducts {
  products = ["Pineapple", "Apples", "Flour"];

  hasProduct(product) {
    // indexOf returns the position of the product in the array,
    // if the position is -1, it means the product does not exist
    return this.products.indexOf(product) !== -1;
  }

  addProduct(product) {
    this.products.push(product);
  }
}

As you can see, we have made modifications to the class without altering the previous functionality, thus complying with the principle.

Liskov Substitution Principle

The principle indicates that if you are using a Rectangle class and then create another class called Square that extends from Rectangle, then any object created from the Rectangle class can be replaced by Square, forcing us to ensure that any child class does not alter the behavior of the parent class.

So we would have a rectangle:

class Rectangle {
  width;
  height;

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  calculateArea() {
    return this.width * this.height;
  }
}

And we have a test written in mocha to check the area:

describe("Validate rectangle area", function () {
  it("The area should be equal to height * width", function () {
    const rectangle = new Rectangle();
    rectangle.setWidth(8);
    rectangle.setHeight(2);
    const area = rectangle.calculateArea();
    assert.equal(area, 16);
  });
});

If we run the test, we find that the area should be equivalent to 16, resulting from multiplying width (8) by height (2).

Now we create a Square class that extends from Rectangle.

class Square extends Rectangle {
  setWidth(width) {
    super.setWidth(width);
    super.setHeight(width);
  }

  setHeight(height) {
    super.setWidth(height);
    super.setHeight(height);
  }
}

To validate that we did not break the functionality of the parent, we will run the test on an object created with the Square class. Running the test, we find that it failed because now a square sets the width and height as the same value, making it impossible to have the area of a rectangle with different sides.

At this point, you might be wondering how to fix it, and you are probably thinking of different possibilities. The first and simplest might be to abstract the logic to a higher class, leaving the code as follows:

class Parallelogram {
  constructor(width, height) {
    this.setWidth(width);
    this.setHeight(height);
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  calculateArea() {
    return this.width * this.height;
  }
}

class Rectangle extends Parallelogram {
  constructor(width, height) {
    super(width, height);
  }
}

class Square extends Parallelogram {
  constructor(side) {
    super(side, side);
  }
}

Interface Segregation Principle

The principle indicates that a class should only implement the interfaces it needs, meaning it should not have to implement methods it does not use. The purpose of this principle is to force us to write small interfaces, aiming to apply the cohesion principle to each interface.

Imagine we have a business selling desktop computers, and we know that all computers should extend from the Computer class. We would have something like this:

class Computer {
  make;
  model;

  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  getMake() {
    return this.make;
  }

  getModel() {
    return this.model;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }
}

class DellComputer extends Computer {
   ...
}

In our business, everything is going great, and now we want to extend our product catalog a bit more, so we decide to start selling laptops. A useful attribute of a laptop is the size of the built-in screen, but as we know, this is only present in laptops and not desktop computers (generalizing). Initially, we might think an implementation could be:

class Computer {
  ...
  constructor() {
    ...
  }
  ...
  setScreenSize(size) {
    this.size = size;
  }
  getScreenSize() {
    return this.size;
  }
}
class HPLaptop extends Computer {
   ...
}

The problem with this implementation is that not all classes, for example, DellDesktop, require the methods to read and write the size of the built-in screen. Therefore, we should think about separating both logics into two interfaces, leaving our code like this:

class Computer {
  make;
  model;

  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  getMake() {
    return this.make;
  }

  getModel() {
    return this.model;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }
}

class BuiltInScreenSize {
  size;
  constructor(size) {
    this.size = size;
  }

  setScreenSize(size) {
    this.size = size;
  }

  getScreenSize() {
    return this.size;
  }
}

class AsusLaptop implements BuiltInScreenSize, Computer {
  ...
}

Everything sounds perfect, but have you noticed the problem? JavaScript only supports one parent class, so the solution would be to apply a mixin. Here is the code using a mixin:

class Computer {
  make;
  model;

  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  getMake() {
    return this.make;
  }

  getModel() {
    return this.model;
  }

  setMake(make) {
    this.make = make;
  }

  setModel(model) {
    this.model = model;
  }
}

const Laptop = (parentClass) => {
  return (
    class extends parentClass {
      constructor(make, model){
        super(make, model);
      }

      setScreenSize(size) {
        this.size

 = size;
      }

      getScreenSize() {
        return this.size;
      }
    }
  )
}

class AsusLaptop extends Laptop(Computer) {
  ...
}

Dependency Inversion Principle

This principle establishes that dependencies should be on abstractions, not concretions. In other words, it requires classes to never depend on other classes, and all relationships should be in an abstraction. This principle has two rules:

  1. High-level modules should not depend on low-level modules. This logic should be in an abstraction.

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

Imagine we have a class that allows us to send an email:

class Email {
  provider;

  constructor() {
    // Create an instance of Google Mail, this code is for demonstration purposes.
    this.provider = gmail.api.createService();
  }

  send(message) {
    this.provider.send(message);
  }
}

var email = new Email();
email.send('hello!');

In this example, we can see that the rule is broken since the Email class depends on the service provider. What if later we want to use Yahoo instead of Gmail?

To solve this, we should remove that dependency and add it as an abstraction.

class GmailProvider {
  constructor() {
    // Create an instance of Google Mail, this code is for demonstration purposes.
    this.provider = gmail.api.createService();
  }
  send(message) {
    this.provider.sendAsText(message);
  }
}
class Email {
  constructor(provider) {
    this.provider = provider;
  }
  send(message) {
    this.provider.send(message);
  }
}
var gmail = new GmailProvider();
var email = new Email(gmail);
email.send('hello!');

This way, we no longer care about the provider or how the provider implements email sending. The Email class only takes care of one thing, asking the provider to send an email.

That’s it for this post about SOLID principles in JavaScript. I would appreciate it if you could leave comments and suggestions on what other topics you would like us to cover.

Robert C. Martin
this guide written by Laserants