Photo by Volodymyr Hryshchenko on Unsplash
From Good to Great: Part 2
Applying SOLID Principles in Your Code
Pre-requisite
Javascript Classes
Javascript Objects
Es6 import and export
Introduction
Welcome, faithful developer. I hope you took out time to use the SOLID patterns we discussed in the previous article.
In today's article, we will be storming through and looking at the remaining two of SOLID and how to apply it to our codebase:
Interface Segregation
Dependency Inversion
Interface Segregation
Although Javascript doesn't have the ability of interfaces, we can replicate the same behavior with class inheritance. This principle states that any descendant class must use all properties of its ancestor.
If we had a Bird class, the properties of the bird would be to run, swim and fly, and if we wanted to make a Dog class, we could just inherit the Bird class, since Dog should be able to run and swim.
However, the Interface Segregation principle states that this inheritance is faulty, the Dog class does not use all the properties i.e flying off the Bird class.
Let's write some code.
class Entity {
constructor(name, attackDamage, health) {
this.name = name;
this.attackDamage = attackDamage;
this.health = health;
}
move() {
console.log(`${this.name} moved`);
}
attack(target) {
console.log(`${this.name} attacked ${target.name} for
${this.attackDamage} damage`);
target.takeDamage(this.attackDamage);
}
takeDamage(amount) {
this.health -= amount;
console.log(`${this.name} has ${this.health} health remaining`);
}
}
class Character extends Entity {}
class Wall extends Entity {
constructor(name, health) {
super(name, 0, health);
}
move() {
return null;
}
attack() {
return null;
}
}
class Turret extends Entity {
constructor(name, attackDamage) {
super(name, attackDamage, -1);
}
move() {
return null;
}
takeDamage() {
return null;
}
}
const turret = new Turret("Turret", 5);
const character = new Character("Character", 3, 100);
const wall = new Wall("Wall", 200);
turret.attack(character);
character.move();
character.attack(wall);
In this code, the
Entity
class is made up of more methods,move()
,attack()
,takeDamage()
, and its descendantsCharacter
,Wall
,Turret
inherit one or more methods from it.The
Wall
andTurret
classes inherit thetakeDamage()
andattack()
methods respectively but exclude the other methods. Meanwhile theCharacter
the class takes all three.This means that the
Wall
andTurret
classes do not satisfy the Interface Segregation principle.
Solution:
// Implement the interfaces in separate classes
class Entity {
constructor(name) {
this.name = name;
}
}
class Character extends Entity {
constructor(name, attackDamage, health) {
super(name);
this.attackDamage = attackDamage;
this.health = health;
}
move() {
console.log(`${this.name} moved`);
}
attack(target) {
console.log(`${this.name} attacked ${target.name} for
${this.attackDamage} damage`);
target.takeDamage(this.attackDamage);
}
takeDamage(amount) {
this.health -= amount;
console.log(`${this.name} has ${this.health} health remaining`);
}
}
class Wall extends Entity {
constructor(name, health) {
super(name);
this.health = health;
}
takeDamage(amount) {
this.health -= amount;
console.log(`${this.name} has ${this.health} health remaining`);
}
}
class Turret extends Entity {
constructor(name, attackDamage) {
super(name);
this.attackDamage = attackDamage;
}
attack(target) {
console.log(`${this.name} attacked ${target.name} for
${this.attackDamage} damage`);
target.takeDamage(this.attackDamage);
}
}
// Use the classes to create instances and call their methods
const turret = new Turret("Turret", 5);
const character = new Character("Character", 3, 100);
const wall = new Wall("Wall", 200);
turret.attack(character);
character.move();
character.attack(wall);
To satisfy the Interface Segregation principle, we made the
Entity
class smaller, to only pass off a name as an inheritance to its descendant classes.All Classes now contain only methods that they implement.
And with that, the Interface Segregation principle is satisfied.
Dependency Inversion
With the interfaces segregated, let's look at the last design pattern of SOLID: Dependency Inversion.
The Dependency Inversion principle is simply against two classes being too dependent on each other. It is similar to the Open/Closed principle but more related to the relationship between two classes.
Imagine, if you were making a robot that talks and a robot that runs. To change the way the robot walks, you might have to change the way the talks because the control of the robot's ability to walk and talk is linked together.
This is what the Dependency Inversion principle is against, you should be able to change the way the robot walks without changing the way the robot talks.
And, if both abilities must be linked, then there should be an abstraction between them, that can serve as a placeholder for one attribute in another but won't necessarily be the attribute itself.
First, a POC(Piece of Code) that violates the Dependency Inversion principle:
class Waiter {
constructor() {
this.order = null;
}
takeOrder(customer) {
this.order = customer.placeOrder();
}
deliverOrder() {
const chef = new Chef();
chef.cook(this.order);
}
}
class Chef {
cook(order) {
console.log(`${order} is served hot and sizzling`);
}
}
class Customer {
placeOrder() {
return "Biryani";
}
}
const waiter = new Waiter();
const customer = new Customer();
waiter.takeOrder(customer);
waiter.deliverOrder();
Check out the
deliverOrder()
method in theWaiter
class, it has to call aChef
class inside of it.If perhaps we wanted to update our
Waiter
to use aChef2
class, we would need to modify theWaiter
class and this violates the Dependency Inversion principle.
Solution:
We should develop an interface, so that even if we change the Chef
class, any class that has a cook
method which will work with the Waiter
class
class Waiter {
constructor(chef) {
this.order = null;
this.chef = chef;
}
takeOrder(customer) {
this.order = customer.placeOrder();
}
deliverOrder() {
// Dependency Inversion
this.chef.cook(this.order);
}
}
class Chef {
cook(order) {
console.log(`${order} is served hot and sizzling`);
}
}
class Customer {
placeOrder() {
return "Biryani";
}
}
const chef = new Chef(); // Dependency Inversion
const waiter = new Waiter(chef);
const customer = new Customer();
waiter.takeOrder(customer);
waiter.deliverOrder();
The
Waiter
the class now takes achef
parameter which can be any class.The
Chef
class is no longer written inside called insideWaiter
class.This property
this.chef
, can now work with any class that has acook
method.
Conclusion
We have seen how to apply the SOLID design principles of Interface Segregation and Dependency Inversion in our JavaScript code.
We learned that Interface Segregation can be implemented by creating separate classes to implement the interfaces instead of inheriting all methods from one parent class and not using some methods.
Dependency Inversion is against two classes being too dependent on each other and can be modified without affecting each other. By applying these principles, we can write better, maintainable, and scalable code.
For the next article titled "Loosely Coupled Objects," we will dive deeper into the concept of decoupling objects and its benefits in creating clean code. We will look at how we can achieve loosely coupled objects by applying the Dependency Injection pattern and other techniques.
Stay tuned!