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!
Top comments (3)
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.
Quick edit (A little pedantic tbh).
In Open Closed Principle I think you meant
filterByLanguage
instead offilterBySize
Cool