SOLID is five design principles intended to make software designs more understandable, flexible, and maintainable. It helps us to think about the right way to build a software system
SOLID is just a set of design principles that all coders and developers should follow to achieve a good design in the software they build. None other than Robert C Martin proposed it. SOLID principles explain how to arrange our functions and data structures and how they can be interconnected.
The main goal of the SOLID principles is that any software should tolerate change and should be easy to understand. If we desire to build quality software, the SOLID principle is essential to be followed.
Most people assume SOLID is only for strongly typed languages or object-oriented languages like JAVA and more. Though Javascript is a loosely typed language, we can easily apply SOLID principles on Javascript code. Let’s see how we can do that in this article.
Any software module in an organization should have one and only one reason to change. It means that any function should be responsible for doing only one thing. For instance, in react we can relate it to the stateless functional component. Let’s see it with an example.
function createUser(name, email){
let re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
let isEmailValid = re.test(email);
if(isEmailValid){
createUserInDabase(name, email)
}
}
In the above example what if the logic for the email changes. The function createUser should have only one reason to change. In our case only if the logic to create User Changes the function should change. So let’s see how we can change that to SRP
function validateRequest(req){
let re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
let isEmailValid = re.test(req.email);
if(isEmailValid){
createUser(req.name, req.email)
}
}
function createUser(name, email){
createUserInDabase(name, email)
}
Any software system should be open for extension and closed for change. This means that the software systems should be allowed to change their behavior by adding new code rather than changing the existing code.
Let’s see an example for the Open-Closed Principle
let allowedRoles = ["admin", "owner"]
function checkPrivilege(user){
if(allowedRoles.includes(user.role)){
return true; //user has privilege
}else{
return false
}
}
Now, what if the software system introduces a new role called guestAdmin and users with guestAdmin roles should also be granted privilege. So here we have to modify existing code to add guestAdmin to the list. So rather we can do like the below example to make it pass the Open-Closed principle.
let allowedRoles = ["admin", "owner"]
function checkPrivilege(user){
if(allowedRoles.includes(user.role)){
return true; //user has privilege
}else{
return false
}
}
addRoles(role){
allowedRoles.push(role)
}
So here we don’t have to modify the existing code rather we can extend it to add a new role
As per the LSP, functions that use references to base classes must be able to use objects of the derived class without knowing it. In simple words, derived classes must be substitutable for the base class. Let’s see the Liskov Substitution Principle with an example:
var License = function(user){
this.calculateFee = function (){
//Logic to calculate Fee
}
}
License.prototype.PersonalLicense = function(user){
this.calculateFee(user)
}
License.prototype.BusinessLicense = function(user){
this.calculateFee(user)
this.getUsers = function(){
//Logic to get all users in Business
}
}
The above example is perfectly in LSP because even if we replace the extended function PersonalLicense instead of License function.
So to conclude LSP states that:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Interface Segregation Principles advises avoiding depending on modules or functions or anything that they don’t use.
But we don’t have an interface by default in Javascript. But we all would have faced situations where we want to do so many things on the constructor of a class. Let’s say some settings we have to do in the constructor. The settings we do should be segregated from the other unwanted settings in the constructor. For example:
class User{
constructor(user){
this.user = user;
this.initiateUser();
}
initiateUser(){
this.name = this.user.name
this.validateUser()
}
}
const user = new User({userProperties, validateUser(){}});
Here, validateUser() function will get invoked in initiateUser constructor call even though it’s not needed all the time. Here comes the Interface Segregation Principle. We can bring this into ISP with the below code:
class User{
constructor(user){
this.user = user;
this.initiateUser();
this.setupOptions = user.options
}
initiateUser(){
this.name = this.user.name
this.setupOptions()
}
}
const user = new User({userProperties, options: {validateUser()}{}});
Here, we pass in options and only if we pass in options it will validate the User. So we are segregating the unwanted logic from the contractor.
Dependency Inversion Principle states that high-level functions should not be dependant on the code that implements low-level functions.
This principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions. In a statically typed language, like Java, this means that the use, import, and include statements should refer only to source modules containing interfaces, abstract classes. In case of Javascript we can see the below example:
axios.get("/someAddress/someResource", function (response) {
this.setState({
value1: response.value1,
value2: response.value2
});
});
The above code is not in DIP. Let’s make it in DIP in the below example:
requestWithAxios("/someAddress/someResource", setResponseInState);
function requestWithAxios(address, setResponseInState){
axios.get("/someAddress/someResource", function (response) {
setResponseInState.setValues(response);
});
}
var setResponseInState ={
setValues: function(response){
this.setState({
value1: response.value1,
value2: response.value2
})
}
}
I hope this gave you a basic understanding of how to apply SOLID principles in Javascript. Applying SOLID principles can make the code cleaner, extensible and easier to understand.
Thanks for reading this article.
I head Tech at Upshotly. We are excited about building tools for modern leaders to help them put their people at the heart of business success. If you think you benefited from this blog, please share it with your friends and co-workers! In case you have any queries, clarification, or an idea for my next blog, please let me know in the comments!