DEV Community

loading...
Cover image for S.O.L.I.D. Principles around You, in JavaScript

S.O.L.I.D. Principles around You, in JavaScript

francescoxx profile image Francesco Ciulla ・4 min read

I would like to thank my friend Oleksii Trekhleb for the contribution to this article.

Oleksii is the original author of this legendary GitHub repository
https://github.com/trekhleb/javascript-algorithms

Follow him on Twitter
https://twitter.com/Trekhleb

The SOLID principles are a set of software design principles, that help us to understand how we can structure our code in order to be robust, maintainable, flexible as much as possible

Here come the S.O.L.I.D. principles:


S - Single Responsibility Principle

Alt Text

Any function must be responsible for doing only ONE thing.
Only one potential change in the software’s specification should be able to affect the specification of the class.

Example : Let's say we want to validate a form, then create a user in a DB

NO

/* A function with such a name is a symptom of ignoring the Single Responsibility Principle
*  Validation and Specific implementation of the user creation is strongly coupled.
*  That's not good
*/ 
validateAndCreatePostgresUser = (name, password, email) => {   

  //Call an external function to validate the user form
  const isFormValid = testForm(name, password, email); 

  //Form is Valid
  if(isFormValid){
    CreateUser(name, password, email) //Specific implementation of the user creation!
  }
}

YES

//Only Validate
validateRequest = (req) => {

  //Call an external function to validate the user form
  const isFormValid = testForm(name, password, email); 

  //Form is Valid
  if(isFormValid){
    createUser(req); // implemented in another function/module 
  }
}

//Only Create User in the Database
createUser = (req) => CreateUser(req.name, req.password, req.email)

/*A further step is to declarel this function in another file
* and import it into this one.
*/

This seems a pretty little change, but decouples the logic of validation from the user creation, which could change in the future, for many reasons!


O - Open-Closed Principle

Alt Text

Software systems must be allowed to change their behavior by adding new code rather than changing the existing code.

Open for extension, but Closed to modification

If we have something like this:

const roles = ["ADMIN", "USER"]
checkRole = (user) => {
  if(roles.includes(user.role)){
    return true; 
  }else{
    return false
  }
}

//Test role
checkRole("ADMIN"); //true
checkRole("Foo"); //false

And we want to add a superuser, for any reason, instead of modifying the existing code (or maybe we just can't modify it), we could do it in another function.

//UNTOUCHABLE CODE!!!
const roles = ["ADMIN", "USER"]
checkRole = (user) => {
  if(roles.includes(user.role)){
    return true; 
  }else{
    return false
  }
}
//UNTOUCHABLE CODE!!!

//We can define a function to add a new role with this function
addRole(role){
  roles.push(role)
}

//Call the function with the new role to add to the existing ones
addRole("SUPERUSER");

//Test role
checkRole("ADMIN"); //true
checkRole("Foo"); //false
checkRole("SUPERUSER"); //true


L - Liskov Substitution Principle

Alt Text

Build software systems from interchangeable parts.

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

class Job {
  constructor(customer) {
    this.customer = customer;
    this.calculateFee = function () {
      console.log("calculate price"); //Add price logic
    };
  }
  Simple(customer) {
    this.calculateFee(customer);
  }
  Pro(customer) {
    this.calculateFee(customer);
    console.log("Add pro services"); //additional functionalities
  }
}



const a = new Job("Francesco");
a.Simple(); 
//Output:
//calculate price


a.Pro();
//Output: 
//calculate price 
//Add pro services...


I - Interface Segregation Principle

Alt Text

Many client-specific interfaces are better than one general-purpose interface.

We don't have interfaces in Javascript, but let's see this example

NO

//Validate in any case
class User {
  constructor(username, password) {
    this.username = username;
    this.password = password;
    this.initiateUser();
  }
  initiateUser() {
    this.username = this.username;
    this.validateUser()
  }

  validateUser = (user, pass) => {
    console.log("validating..."); //insert validation logic here!
  }
}
const user = new User("Francesco", "123456");
console.log(user);
// validating...
// User {
//   validateUser: [Function: validateUser],
//   username: 'Francesco',
//   password: '123456'
// }

YES

//ISP: Validate only if it is necessary
class UserISP {
  constructor(username, password, validate) {
    this.username = username;
    this.password = password;
    this.validate = validate;

    if (validate) {
      this.initiateUser(username, password);
    } else {
      console.log("no validation required"); 
    }
  }

  initiateUser() {
    this.validateUser(this.username, this.password);
  }

  validateUser = (username, password) => {
    console.log("validating...");
  }
}

//User with validation required
console.log(new UserISP("Francesco", "123456", true));
// validating...
// UserISP {
//   validateUser: [Function: validateUser],
//   username: 'Francesco',
//   password: '123456',
//   validate: true
// }


//User with no validation required
console.log(new UserISP("guest", "guest", false));
// no validation required
// UserISP {
//   validateUser: [Function: validateUser],
//   username: 'guest',
//   password: 'guest',
//   validate: false
// }


D - Dependency Inversion Principle

Alt Text

Abstractions must not depend on details.

Details must depend on abstractions.

NO

//The Http Request depends on the setState function, which is a detail
http.get("http://address/api/examples", (res) => {
 this.setState({
  key1: res.value1,
  key2: res.value2,
  key3: res.value3
 });
});

YES

//Http request
const httpRequest = (url, setState) => {
 http.get(url, (res) => setState.setValues(res))
};

//State set in another function
const setState = {
 setValues: (res) => {
  this.setState({
    key1: res.value1,
    key2: res.value2,
    key3: res.value3
  })
 }
}

//Http request, state set in a different function
httpRequest("http://address/api/examples", setState);

In Conclusion...

The main goal of the SOLID principles is that any software should tolerate change and should be easy to understand.

The S.O.L.I.D. principles can be very useful to write code:

  • Easy to understand
  • Where things are where they're supposed to be
  • Where classes do what they were intended to do
  • That can be easily adjusted and extended without bugs
  • That separates the abstraction from the implementation
  • That allows to easily swap implementation (Db, Api, frameworks, ...)
  • Easily testable

Discussion

pic
Editor guide
Collapse
kelseyleftwich profile image
Kelsey Leftwich

The illustrations in this article are great!

Collapse
francescoxx profile image
Francesco Ciulla Author

Yes indeed they are!

Collapse
jeckerson profile image
Jeckerson

This:

if(roles.includes(user.role)){
    return true; 
}else{
    return false;
}

Can be transformed to this:

if (roles.includes(user.role)) {
    return true; 
}

return false;

Or even to this:

return roles.includes(user.role);
Collapse
francescoxx profile image
Francesco Ciulla Author

Hello Jeckson,

of course it can be transformed like that, the idea here is to be as clear as possible! Feel free to use a more concise syntax

Collapse
evrtrabajo profile image
Emmanuel Valverde Ramos

I think that this code:

/* A function with such a name is a symptom of ignoring the Single Responsibility Principle
*  Validation and Specific implementation of the user creation is strongly coupled.
*  That's not good
*/ 
validateAndCreatePostgresUser = (name, password, email) => {   

  //Call an external function to validate the user form
  const isFormValid = testForm(name, password, email); 

  //Form is Valid
  if(isFormValid){
    CreateUser(name, password, email) //Specific implementation of the user creation!
  }
}

Should be somethig like:

/* A function with such a name is a symptom of ignoring the Single Responsibility Principle
*  Validation and Specific implementation of the user creation is strongly coupled.
*  That's not good
*/ 
validateAndCreatePostgresUser = (name, password, email) => {   

  // Throw an exception
  testForm(name, password, email); 

  //Specific implementation of the user creation!
  CreateUser(name, password, email);

}
Collapse
trekhleb profile image
Oleksii Trekhleb

Well done Francesco! I was glad to have a collaboration with you!

Collapse
francescoxx profile image
Francesco Ciulla Author

Me too Oleksii, see you next time!

Collapse
lirantal profile image
Liran Tal

Thanks for sharing Francesco. For the open/close principle I usually give the example of maintaining a mapper instead of a switch case, and found that to be a helpful example to devs. Keep it up!

Collapse
francescoxx profile image
Francesco Ciulla Author

Hello Liran! Thanks for the constructive help and congratulations fro your hat! :D

Collapse
lirantal profile image
Liran Tal

hah thanks! :)

Collapse
russ profile image
Russ

Love the illustrations, thank you :)

If I'm correctly understanding it Interface Segregation Principle and Dependency Inversion Principle are very similiar?

Collapse
studioic246 profile image
studio indie corner

This is an amazing post. I will keep it close to heart going forward.

Collapse
francescoxx profile image
Francesco Ciulla Author

Thanks for the kind words!

Collapse
linuxnerd profile image
Abhishek Prakash

I feel the same as @kelseyleftwich . The illustrations are perfect! A good and concise post.

Collapse
francescoxx profile image
Francesco Ciulla Author

Yes i totally agree with that!