SOLID Principles in JavaScript

Published on
Authors

Like any other programming language, JavaScript is also subject to the principles outlined in SOLID.

SOLID consists of 5 concepts that we can use to make our programs better. They are:

  • Single responsibility principle
  • Open / closed principles
  • Liskov Substitution principle
  • Interface segregation principle
  • Dependency inversion principle

In this article, we’ll look at each and see how we can apply them to JavaScript programs.

Single Responsibility Principle

The single responsibility principle says that each of our classes has to be only used for one purpose.

We need this so that we don’t have to change code as often when something changes. It’s also hard to understand what the class is doing if it’s doing many things.

Unrelated concepts in the same class also make comprehending the purpose of the code harder.

For example, we can write something like the following to follow the single responsibility principle:

class Rectangle {
	constructor(length, width) {
		this.length = length;
		this.width = width;
	}
	get area() {
		return this.length * this.width;
	}
}

The Rectangle class above only has the length and width of a rectangle as members and lets us get the area from it.

It does nothing else, so it follows the single responsibility principle.

A bad example would be:

class Rectangle {
	constructor(length, width) {
		this.length = length;
		this.width = width;
	}
	get area() {
		return this.length * this.width;
	}
	createCircle() {}
}

We should have a createCircle method in a Rectangle class since they’re unrelated concepts.

Open/Closed Principle

The open/closed principle states that a piece of software is open for extension but closed for modification.

This means that we should be able to add more functionality without changing existing code.

For example, if we have the following Rectangle class:

class Rectangle {
	constructor(length, width) {
		this.length = length;
		this.width = width;
	}
	get area() {
		return this.length * this.width;
	}
}

Then if we want to add a function to for calculating its perimeter, we can do it by adding a method to do it as follows:

class Rectangle {
	constructor(length, width) {
		this.length = length;
		this.width = width;
	}
	get area() {
		return this.length * this.width;
	}
	get perimteter() {
		return 2 * (this.length + this.width);
	}
}

As we can see, we didn’t have to change existing code to add it, which satisfies the open/closed principle.

Liskov Substitution Principle

This principle states that if we have a parent class and a child class, then we can interchange the parent and child class without getting incorrect results.

This means that the child class must implement everything that’s in the parent class. The parent class serves the class has the base members that child classes extend from.

For example, if we want to implement classes for a bunch of shapes, we can have a parent Shape class, which are extended by all classes by implementing everything in the Shape class.

We can write the following to implement some shape classes and get the area of each instance:

class Shape {
    get area() {
        return 0;
    }
}
class Rectangle extends Shape {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
    get area() {
        return this.length * this.width;
    }
}
class Square extends Shape {
    constructor(length) {
        super();
        this.length = length;
    }
    get area() {
        return this.length ** 2;
    }
}
class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    get area() {
        return Math.PI * (this.radius ** 2);
    }
}
const shapes = [new Rectangle(1, 2), new Square(1, 2), new Circle(2), ]
for (let s of shapes) {
    console.log(s.area);
}

Since we override the area getter in each class that extends Shape , we get the right area for each shape since the correct code is run for each shape to get the area.

Interface Segregation Principle

The interface segregation principle states that “clients shouldn’t be forced to depend on interfaces that they don’t use.”

This means that we shouldn’t impose the implementation of something if it’s not needed.

JavaScript doesn’t have interfaces, so this principle doesn’t apply directly since it doesn’t enforce the implementation of anything via interfaces.

Dependency Inversion Principle

This principle states that high-level modules shouldn’t depend on low-level modules and they both should depend on abstractions, and abstractions shouldn't depend upon details. Details should depend upon abstractions.

This means that we shouldn’t have to know any implementation details of our dependencies. If we do, then we violated this principle.

We need this principle because if we do have to reference the code for the implementation details of a dependency to use it, then when the dependency changes, there’s going to be lots of breaking changes to our own code.

As software gets more complex, if we don’t follow this principle, then our code will break a lot.

One example of hiding implementation details from the code that we implement is the facade pattern. The pattern puts a facade class in front of the complex implementation underneath so we only have to depend on the facade to use the features underneath.

If the underlying classes change, then only the facade has to change and we don’t have to worry about making changes to our own code unless the facade has breaking changes.

For example, the following is a simple implementation of the facade pattern:

class ClassA {}
class ClassB {}
class ClassC {}
class Facade {
    constructor() {
        this.a = new ClassA();
        this.b = new ClassB();
        this.c = new ClassC();
    }
}
class Foo {
    constructor() {
        this.facade = new Facade();
    }
}

We don’t have to worry about ClassA , ClassB and ClassC to implement the Foo class. As long as the Facade class doesn’t change, we don’t have to change our own code.

Conclusion

We should follow the SOLID principle to write code that’s easy to maintain.

To following SOLID, we have to write classes that only do one thing.

Our code has to open for extension but closed for modification. This reduces the chance of messing up the existing code.

Parent and child classes have to be interchangeable when we switch them. When we switch them, the result still has to be correct.

Finally, we should never have to depend on the implementation details of any piece of code that we reference so that we won’t end up with lots of breaking changes when something changes.

This lets us reduce the coupling between modules.