loading...
Cover image for 5 SOLID principles with JavaScript. How to make your code SOLID

5 SOLID principles with JavaScript. How to make your code SOLID

denisveleaev profile image Denis Veleaev ・4 min read

Hi 👋! I'm Denis.

SOLID principles are strictly related to design patterns. It's important to know design patterns because it's a hot topic for an interview. If you know them, you'll easily understand more sophisticated programming paradigms, architectural patterns, and language features such as reactive programming, flux architecture (Redux), generators in JavaScript, etc.

What are SOLID principles?

SOLID stands for

  • S — Single responsibility principle
  • O — Open closed principle
  • L — Liskov substitution principle
  • I — Interface segregation principle
  • D — Dependency Inversion principle

These 5 principles will guide you on how to write better code. Though they come from object-oriented programming. I know it's very daring to call JavaScript an object-oriented language :) Regardless, I promise that if you understand these principles, then when you design your next solutions, you will definitely ask yourself "Hey, am I violating the Single-responsibility principle?".

So, let's begin

S — Single responsibility principle

It's probably the easiest principle, and at the same time, the most misunderstood one.

A module should be responsible for only one actor. As a consequence, it has only one reason to change

Example

Let's take a look at the following code:

class TodoList {
  constructor() {
    this.items = []
  }

  addItem(text) {
    this.items.push(text)
  }

  removeItem(index) {
    this.items = items.splice(index, 1)
  }

  toString() {
    return this.items.toString()
  }

  save(filename) {
    fs.writeFileSync(filename, this.toString())
  }

  load(filename) {
    // Some implementation
  }
}

Ooops. Even though from first glance, this class seems to be fine, it violates the Single responsibility principle. We added second responsibility to our TodoList class which is the management of our database.

Let's fix the code so that it complies with the "S" principle.

class TodoList {
  constructor() {
    this.items = []
  }

  addItem(text) {
    this.items.push(text)
  }

  removeItem(index) {
    this.items = items.splice(index, 1)
  }

  toString() {
    return this.items.toString()
  }
}

class DatabaseManager {
  saveToFile(data, filename) {
    fs.writeFileSync(filename, data.toString())
  }

  loadFromFile(filename) {
    // Some implementation
  }
}

Thus our code has become more scalable. Of course, it's not so obvious when we're looking at small solutions. When applied to a complex architecture, this principle takes on much more meaning.

O — Open closed principle

Modules should be open for extension but closed for modification

That means that if you want to extend a module's behavior, you won't need to modify the existing code of that module.

Example

class Coder {
  constructor(fullName, language, hobby, education, workplace, position) {
    this.fullName = fullName
    this.language = language
    this.hobby = hobby
    this.education = education
    this.workplace = workplace
    this.position = position
  }
}

class CoderFilter {
  filterByName(coders, fullName) {
    return coders.filter(coder => coder.fullName === fullName)
  }

  filterBySize(coders, language) {
    return coders.filter(coder => coder.language === language)
  }

  filterByHobby(coders, hobby) {
    return coders.filter(coder => coder.hobby === hobby)
  }
}

The problem with CoderFilter is that if we want to filter by any other new property we have to change CodeFilter's code. Let's solve this problem by creating a filterByProp function.

const filterByProp = (array, propName, value) =>
  array.filter(element => element[propName] === value)

L — Liskov substitution principle

A principle with the most confusing name. What does it mean?

If you have a function, that works for a base type, it should work for a derived type

Let's go with a classic example

Example

class Rectangle {
  constructor(width, height) {
    this._width = width
    this._height = height
  }

  get width() {
    return this._width
  }
  get height() {
    return this._height
  }

  set width(value) {
    this._width = value
  }
  set height(value) {
    this._height = value
  }

  getArea() {
    return this._width * this._height
  }
}

class Square extends Rectangle {
  constructor(size) {
    super(size, size)
  }
}

const square = new Square(2)
square.width = 3
console.log(square.getArea())

Guess what will be printed to the console. If your answer is 6, you are right. Of course, the desired answer is 9. Here we can see a classic violation of the Liskov substitution principle.

By the way, to fix the issue you can define Square this way:

class Square extends Rectangle {
  constructor(size) {
    super(size, size)
  }

  set width(value) {
    this._width = this._height = value
  }

  set height(value) {
    this._width = this._height = value
  }
}

I — Interface segregation principle

Clients should not be forced to depend upon interfaces that they do not use

There are no interfaces in JavaScript. There is a way to mimic their behavior, but I don't think there's much sense. Let's better adapt the principle to the js world.

Example

Let's define an "abstract" Phone class which will play role of the interface in our case:

class Phone {
  constructor() {
    if (this.constructor.name === 'Phone')
      throw new Error('Phone class is absctract')
  }

  call(number) {}

  takePhoto() {}

  connectToWifi() {}
}

Can we use it to define an iPhone?

class IPhone extends Phone {
  call(number) {
    // Implementation
  }

  takePhoto() {
    // Implementation
  }

  connectToWifi() {
    // Implementation
  }
}

Okay, but for an old Nokia 3310 this interface will violate the "I" principle

class Nokia3310 extends Phone {
  call(number) {
    // Implementation
  }

  takePhoto() {
    // Argh, I don't have a camera
  }

  connectToWifi() {
    // Argh, I don't know what wifi is
  }
}

D — Dependency Inversion principle

High-level modules should not depend on low-level modules

Let's take a look at the following example:

Example

class FileSystem {
  writeToFile(data) {
    // Implementation
  }
}

class ExternalDB {
  writeToDatabase(data) {
    // Implementation
  }
}

class LocalPersistance {
  push(data) {
    // Implementation
  }
}

class PersistanceManager {
  saveData(db, data) {
    if (db instanceof FileSystem) {
      db.writeToFile(data)
    }

    if (db instanceof ExternalDB) {
      db.writeToDatabase(data)
    }

    if (db instanceof LocalPersistance) {
      db.push(data)
    }
  }
}

In this case, a high-level module PersistanceManager depends on the low-level modules, which are FileSystem, ExternalDB, and LocalPersistance.

To avoid the issue in this simple case we should probably do something like this:

class FileSystem {
  save(data) {
    // Implementation
  }
}

class ExternalDB {
  save(data) {
    // Implementation
  }
}

class LocalPersistance {
  save(data) {
    // Implementation
  }
}

class PersistanceManager {
  saveData(db, data) {
    db.save(data)
  }
}

Of course, this is an oversimplified example, but you've got the point.

Conclusion

The value of SOLID principles is not obvious. But if you ask yourself "Am I violating SOLID principles" when you design your architecture, I promise that the quality and scalability of your code will be much better.

Thanks a lot for reading!
Feel free to follow me here on DEV.to and also on Twitter (@DenisVeleaev)

Peace!

Discussion

pic
Editor guide
 

Our year long project received two major specification changes last week.

The work involved major project folder moves as well as major gui layout and work flow changes.

Fortunately we adopted SOLID from the start.

Result was four days of work.

Had we not followed SOLID, the time would have been four to eight weeks.

It's much easier to work on small Singlely Responsible parts whose changes don't affect any upstream or downstream part.

To weave in and out of monolithic code is just bad.