S.O.L.I.D Stands for first five object-oriented design principle by Robert C.Martin.
SOLID principles can be used to design and develop extensible and easy-to-maintain software. By using these principles in object-oriented programming developers can create and maintain the codebase very easily.
SOLID stands for:
- Single-responsibility principle
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency Inversion Principle
In this article, we will learn about the principle of the codebase.
Single-responsibility principle
A class should have only one job.
This principle states and recommends that a class should have only one responsibility. If a class contains multiple responsibilities then it becomes coupled. The coupling creates a chain of dependency which is never good for software.
This principle also applies to microservices design where we allocate one responsibility to one service.
For example, consider this class:
constructor(name: string){ }
getHeroName() { }
saveHeros(a: Hero) { }
}
This class violates SRP. Here is why.
This class has more than responsibility. It is managing the properties of super hero’s and also handling the database. If there is any update in database management functions then it will affect the properties management functions as well hence resulting in coupling.
In order to meet the SRP, we just create another class that will handle the sole responsibility of database management.
constructor(name: string){ }
getHeroName() { }
}
class SuperHeroDB {
getHeros(a: Hero) {}
saveHeros(a: Hero) { }
}
This way our code becomes more cohesive and less coupled.
Open-closed principle
Software entities(Classes, modules, functions) should be open for extension, not modification.
This simply means that the classes, functions should not be modified whenever we need to develop new features. We should extend the entities not modify it.
Let’s learn this with our super hero’s class.
constructor(name: string){ }
getHeroName() { }
}
We want to iterate through a list of super hero’s and return their weapon of choice.
constructor(name: string){ }
getHeroName() { // ... }
getWeapons(herosName) {
for(let index = 0; index <= herosName.length; index++) {
if(herosName[index].name === 'thor') {
console.log('storm breaker');
}
if(herosName[index].name === 'captainamerica') {
console.log('Shield');
}
}
}
}
The function getWeapons() does not meet the open-closed principle because it cannot be closed against new kind of superheroes.
if we add a new superhero say Iron man. So we need to change the function and add the new code like this.
constructor(name: string){ }
getHeroName() { // ... }
getWeapons(herosName) {
for(let index = 0; index <= herosName.length; index++) {
if(herosName[index].name === 'thor') {
console.log('storm breaker');
}
if(herosName[index].name === 'captainamerica') {
console.log('Shield');
}
if(herosName[index].name === 'ironman') {
console.log('the suit');
}
}
}
}
If you observe, for every new superhero, a new logic is added to the getWeapons() function. As per the open-closed principle, the function should be open for extension, not modification.
Here is how we can make the codebase meets the standard of OCP.
constructor(name: string) { }
getWeapons() { }
}
class Thor extends SuperHero {
getWeapons() {
return 'storm breaker';
}
}
class CaptainAmerica extends SuperHero {
getWeapons() {
return 'Shield';
}
}
class Ironman extends SuperHero {
getWeapons() {
return 'Suit';
}
}
function getWeapons(a: Array<superhero>) {
for (let index = 0 ; index <= a.length; index++) {
console.log(a[index].getWeapons())
}
}
getWeapons(superheros);
This way we do not need to modify the code whenever a new superhero is required to add. We can just create a class and extends it with the base class.
Liskov substitution principle
A sub-class must be substitutable for its super-class.
This principle states that every subclass/derived class should acts as a substitute to their base/parent class.
For example, consider this code.
constructor(name: string) { }
getWeapons() { }
}
class Thor extends SuperHero {
getThorWeapons() {
return 'storm breaker';
}
}
class CaptainAmerica extends SuperHero {
getCaptainWeapons() {
return 'Shield';
}
}
class Ironman extends SuperHero {
getIronManWeapons() {
return 'Suit';
}
}
function getWeapons(a: Array<superhero>) {
for (let index = 0 ; index <= a.length; index++) {
console.log(a[index].getWeapons())
}
}
getWeapons(superheros);
The code shown above does not adhere to LSP. To do so, we need to have a function that is in the base class as well as in the derived class hence the derived class acts as a substitute to their base class.
constructor(name: string) { }
getWeapons() { }
}
class Thor extends SuperHero {
getWeapons() {
return 'storm breaker';
}
}
function getWeapons(a: Array<superhero>) {
for (let index = 0 ; index <= a.length; index++) {
console.log(a[index].getWeapons())
}
}
getWeapons(superheros);
When a new class is derived, it should implement the getWeapons() function, like this.
getIronManWeapons() {
return 'Suit';
}
}
Interface segregation principle
A Client should not be forced to depend upon interfaces and methods that they do not use.
This principle states that the client using the interface should not be forced upon using the methods they don’t need. For example, consider this interface.
stormBreaker();
Shield();
Hammer();
Suit();
}
This interface has methods that are dealing with various weapons. If any class tries to use this implement, they have to use all the methods.
stormBreaker() {}
Shield() {}
Hammer() {}
Suit() {}
}
class Ironman implements Weapons {
stormBreaker() {}
Shield() {}
Hammer() {}
Suit() {}
}
If we add a new method in the interface, all the other classes must declare that method or error will be thrown. To make the interface follow LSP, we need to segregate the weapons methods to different interfaces.
Hammer();
}
interface IronmanWeapon {
Suit();
}
interface CaptainAmericaWeapon {
Sheild();
}
class thor implements ThorWeapon {
Hammer() { // code }
}
This will make sure that the client is only implementing the methods that they should use.
Dependency Inversion Principle
Depend on Abstractions not on concretions.
That means,
1. High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
2. Abstractions should not depend on details. Details should depend upon abstractions.
For example, consider this code:
class SuperHero {
constructor(private db: pool) {}
saveHeroes() {
this.db.save();
}
}
Here, class SuperHero is a high-level component whereas a pool variable is a low-level component. This violates the DIP. In order to make it adhere to the principle, we need to make the following change.
mysql.createPool({ // other details})
}
class SuperHero {
constructor(private db: Connection) {}
saveHeroes() {
this.db.save();
}
}
With the little change in the code above, we can see that both the high-level and low-level modules depend on abstraction.
Conclusion
We learned the five principles every software developer must adhere to. It might be a little scary at first to follow all these principles, but with steady practice and adherence, it will become a part of us and will have a huge impact on the maintenance of our codebase.
If you have any questions/errors/improvements, comment down and let me know.
Further Study
Facebook Login Implementation Using Nodejs and Express
Top 5 Node.js Frameworks to Increase Coding Productivity
15 Best Visual Studio Code Extensions For Web Development