DEV Community

Saurav Ghimire
Saurav Ghimire

Posted on

SOLID Principles

SOLID principles: Solid principles are the combination of 5 major principles. They are as follows:
1.Single responsibility principle
2.Liskov substitution principle
3.Open/closed principle
4.Interface segregation Principle
5.Dependency Inversion principle

  1. Single responsibility principle -> Robert C. Martin expresses the principle as "A class should have only one reason to change" which means that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

Code for single responsibility principle:
//this is a bad practice
class Usersettings{
constructor(user){
this.user= user;
}
} changesettings(settings){
if(this.verifycredentials()){
//.....
}
}
verifycredentials(){
}
}

//the good practice here would be

class UserAuth{
constructor(user){
this.user = user;
}
verifycredentials(){
//...
}
class Usersettings{
constructor(user){
this.user = user;
this.auth = new UserAuth(user);
}
changesettings(settings){
if(this.auth.verifycredential()){
//........
}
}
}
here a class has been assigned a single responsibility, that is one class is responsible for usersettings of adding new user, and the other class is responsible for verifying the user credentials.

  1. Liskov substitution principle -> Introduced by Barbara liskov, the principle states that "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. It means that if a program module is using a Base class, then the reference to the base class can be replaced with a derived class without affecting the functionality of the program module. We can also state that the derived types must be substitutable for their base types. Bad example where Liskov substitution is not used:

class Rectangle{
constructor(){
this.width = 0;
this.height = 0;
}
setColor(color){
//...
}
render(area){
///....
}
setWidth(width){
this.width = width;
}
setHeight(height){
this.height = height;
}
getarea(){
return this.width * this.height;
}
}
class square extends Rectangle{
setWidth(width){
this.width = width;
this.height= width;
}
setHeight(height){
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles){
rectangles.ForEach((rectangle=>{
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); //Bad , returns 25 for square, this returns 20
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

//The good way to do this would be ,

class Shape{
setColor(color){
//......
}
render(area){
//....
}
}
class Rectangle extends Shape{
constructor(width, height){
super();
this.length = length;
}
getArea(){
return this.length * this.length;
}
}
function renderLargeShapes(shapes){
shapes.forEach((shape)=>{
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4,5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

3.Open/Closed principles:
->"Software Entities should be open for extension but closed for modification"
The design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code.
The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible the existing code unchanged.

The bad way of the code writing in this would be:
//Bad
var iceCreamFlavors = ["chocolate", "vanilla"];
var iceCreamMaker={
makeIceCream(flavor){
if(iceCreamFlavors.indexOf(flavor)>-1){
console.log("Great success, there is icecream for you")
}else{
console.log("there is no icecream for you, epic fail")
}
}
}
//good way
var iceCreamFlavors=["chocolate", "vanilla"];
var iceCreamMaker ={
makeIceCream(flavor){
if(iceCreamFlavors.indexOf(flavor)>-1){
console.log("Great Success, there is icecream for you")
}else{
console.log("There is no icecream for you , epic fail")
}
}
addFlavor(flavor){
iceCreamFlavors.push(flavor);
}
}
export default iceCreamMaker;
//if we see, then in the first version of the code, we dont have a place for adding an ice cream flavor, in order to add a flavor in the first code, we have to add an element in the array list, which means that the code gets modified, but our open/close principle doesn't allow that to happen, a code can be extended but not modified. So, in the second code, we added a function 'addFlavor(flavor)' with the parameter flavor inside the bracket which can push a different flavor of icecream inside the array,which resulted in the addition/extension of the code but not modification. So the good code, or the second follows the Open/closed principle.

4.Interface segregation principle
-> Many client specific interfaces are better than one general purpose interface.
We should not enforce clients to implement interfaces that they donot use. Instead of creating one big interface, we can break it down to smaller interfaces.

//BAD
class DOMTraverser{
constructor(settings){
this.settings = settings;
this.setup();
}
setup(){
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse(){
//....
}
}
const $= new DOMTraverser({
rootNode:document.getElementByTagName('body'),
animationModule(){} // Most of the time we don't need to animate while traversing
});

//GOOD way to do this

class DOMTraverser{
constructor(settings){
this.settings = settings;
this.options = options;
this.setup();
}
setup(){
this.rootNode = this.settings.rootNode;
this.saveOptions();
}
setupOptions(){
if(this.options.animationModule){
//.....
}
}
traverse(){
//.....
}
}
const $= newDOMTraverser{
rootNode: document.getElementByTagName('body'),
options:{
animationModule(){}
}
});

  1. Dependency Inversion Principle ->One should depend upon abstractions and not concretions. Abstractions should not depend upon details whereas details should depend upon abstractions. High level modules should not depend upon low level modules.

//BAD code writing:
class InventoryRequester{
constructor(){
this.REQ_METHODS = ['HTTP'];
}
requestingItem(item){
//....
}
}
class InventoryTracker{
constructor(items){
this.items = items;

//BAD: we have created a dependency on a specific request implementation.
//we should have requestItems depend on a single request method:'request'
this.requester = newInventoryRequester();
}
requestItems(){
this.items.forEach((item)=>{
this.requester.requestItem(item);
});
}
}
cost InventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

//GOOD
class InventoryRequester{
constructor(){
this.REQ_METHODS = ['HTTP'];
}
requestingItem(item){
//....
}
}
class InventoryRequesterv2{
constructor(){
this.REQ_METHODS = ['WS'];
}
requestItem(item){
//....
}
}

//By constructing our dependencies externally and injecting them, we can easily substitute our request module for a fancy new one that uses websockets.
const InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
InventoryTracker.requestItems();

What happens if we do not follow solid principles?
-> Some of the cases may occur if we do not follow solid principles. They are:
a. We may end up with tight or strong coupling of code with many other modules/applications.
b.Tight coupling causes time to implement any new requirement features or any bug fixes and sometimes, it creates unknown issues.
c. We may end up with a code that is not testable in nature.
d. There would be a chance of duplication of code.

what are the advantages of following the solid principles ?
-> Following the solid principles helps us to:
a. Achieve reduction in the code complexity
b. Increase readability, extensibility and maintenance.
c. Reduces error and implementations and reusability
d. Achieves better testability
e. Reduces tight coupling.

Discussion (0)