SOLID Javascript
This post will cover what the S.O.L.I.D. principle is and show examples of each in Javascript so that you will have a SOLID understanding of it.
Let’s break these down and show examples:
Single Responsibility Principle
A class should have one and only one reason to change, meaning that a class should only have one job.
Let’s say we have an app that has a login page that asks for a username and a password prompt. This is a way to write a function that accomplishes a login page, but it doesn’t follow the Single Responsibility Principle:
const mainMenu = () => {
console.log("Enter Username:");
console.log("Enter Password:");
}
Notice that mainMenu is doing 2 tasks. This isn’t good because it wouldn’t be possible to just give a prompt for just a username or password. We can accomplish this by splitting mainMenu into smaller functions for usernames and passwords:
const login = () => {
console.log("Enter Username:");
}const password = () => {
console.log("Enter Password:");
}const mainMenu = () => {
login()
password()
}
Splitting up the work into separate functions will make it easier to modify login and passwords in the future if we need to.
Open Closed Principle
Software components should be open for extension, but closed for modification
This means that if someone wants to extend a module’s behavior, they won’t need to modify existing code if they don’t want to. You’ve failed the Open Closed Principle if you have to your module and make a modification in order to extend it,. A bad example:
const sandwiches = ['turkey', 'ham'];
const sandwichMaker = {
makeSandwich(sandwich) {
if (sandwiches.indexOf(sandwich) > -1) {
console.log('You made a sandwich!');
} else {
console.log('You don't get a sandwich!');
}
},
};
export default sandwichMaker;
Notice that there is no way to add a sandwich to the sandwiches array without hard-coding the sandwiches array. Let’s modify this to make a good example:
const sandwiches = ['turkey', 'ham'];
const sandwichMaker = {
makeSandwich(sandwich) {
if (sandwiches.indexOf(sandwich) > -1) {
console.log('You made a sandwich!');
} else {
console.log(`You don't get a sandwich!`);
}
},
addSandwich(sandwich) {
sandwiches.push(sandwich);
},
};
export default sandwichMaker;
Now we can add a new sandwich to the sandwiches array without having to modifying the sandwiches array directlyw.
Liskov’s Substitution Principle
Derived types must be completely substitutable for their base types. extended classes should be able to fit into out app without failure.
In Javascript terms, this principle means that functions that are derived from other functions shouldn’t break the parent function. A bad example:
const sandwiches = ['turkey', 'ham'];
const sandwichMaker = {
makeSandwich(sandwich) {
if (sandwiches.indexOf(sandwich) > -1) {
console.log('You made a sandwich!');
} else {
console.log(`You don't get a sandwich!`);
}
},
addSandwich(sandwich) {
sandwiches.push(sandwich);
},
};const openDeli = () => {
sandwiches.push("italian")
sandwichMaker.makeSandwich("turkey")
return sandwiches
}
openDeli breaks sandwichMaker because it doesn’t use addSandwich to add to the sandwiches array. An example that uses the principle:
const sandwiches = ['turkey', 'ham'];
const sandwichMaker = {
makeSandwich(sandwich) {
if (sandwiches.indexOf(sandwich) > -1) {
console.log('You made a sandwich!');
} else {
console.log(`You don't get a sandwich!`);
}
},
addSandwich(sandwich) {
sandwiches.push(sandwich);
},
};const openDeli = () => {
sandwichMaker.makeSandwich("turkey")
sandwichMaker.addSandwich("roast beef")
return sandwiches
}
openDeli now uses addSandwich and doesn’t contradict the sandwichMaker function.
Interface Segregation Principle
Clients should not be forced to implement unnecessary methods that they won’t use.
This principle is pretty self-explanatory. You shouldn’t implement methods that are unnecessary in your code. Say in our deli example, you wouldn’t need a function to make ice cream because it isn’t relevant to working in a deli. But say it was necessary, you could make a separate iceCreamMaker function to facilitate that need:
const iceCreams = ['vanilla', 'chocolate'];
const iceCreamMaker = {
makeIceCream(iceCream) {
if (iceCreams.indexOf(iceCream) > -1) {
console.log('You made a iceCream!');
} else {
console.log(`You don't get a iceCream!`);
}
},
addIceCream(iceCream) {
iceCreams.push(iceCream);
},
};
const openIceCreamStore = () => {
iceCreams.addIceCream("rocky road")
iceCreamMaker.makeIceCream("vanilla")
return iceCreams
}
Dependency Inversion Principle
Depend on abstractions, not on concretions.
- High Level Modules Should Not Depend On Low Level Modules
- Abstraction Should Not Depend on Details, Details Should Depend on Abstractions
Let’s look at what we have so far for sandwichMaker and iceCreamMaker:
const sandwiches = ['turkey', 'ham'];
const sandwichMaker = {
makeSandwich(sandwich) {
if (sandwiches.indexOf(sandwich) > -1) {
console.log('You made a sandwich!');
} else {
console.log(`You don't get a sandwich!`);
}
},
addSandwich(sandwich) {
sandwiches.push(sandwich);
},
};const openDeli = () => {
sandwichMaker.makeSandwich("turkey")
sandwichMaker.addSandwich("roast beef")
return sandwiches
}const iceCreams = ['vanilla', 'chocolate'];
const iceCreamMaker = {
makeIceCream(iceCream) {
if (iceCreams.indexOf(iceCream) > -1) {
console.log('You made a iceCream!');
} else {
console.log(`You don't get a iceCream!`);
}
},
addIceCream(iceCream) {
iceCreams.push(iceCream);
},
};
const openIceCreamStore = () => {
iceCreams.addIceCream("rocky road")
iceCreamMaker.makeIceCream("vanilla")
return iceCreams
}
This doesn’t seem like a very efficient way to make sandwiches and ice cream. Most of the functions are exactly same. Let’s make an abstraction of both of these functions:
let stock = [];
const itemMaker = {
makeItem(type, item) {
if (stock.indexOf(item) > -1) {
console.log(`You made a ${type}!`);
} else {
console.log(`You don't get a ${type}!`);
}
},
addItem(item) {
stock.push(item);
},
};const openIceCreamStore = () => {
stock = []
itemMaker.addItem("vanilla")
itemMaker.addItem("chocolate")
itemMaker.addItem("rocky road")
itemMaker.makeItem("ice cream", "vanilla")
return stock
}const openDeli = () => {
stock = []
itemMaker.addItem("turkey")
itemMaker.addItem("ham")
itemMaker.addItem("roast beef")
itemMaker.makeItem("sandwich", "turkey")
return stock
}
I made an itemMaker function that can take in whatever type of items a user would want to use. Looking at the bullet points above, itemMaker doesn’t depend on openDeli or openIceCreamStore to work. Any input will work with itemMaker, plus we also have less repetitive code!
Conclusion
I hope that this breakdown of the S.O.L.I.D principle helps you apply it to your Javascript code. Thanks for reading!