The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules and best practices to follow while designing a class structure.
So, what is SOLID? How does it help us write better code? Simply put, these principles encourage us to create more maintainable, understandable and clean code.

Following the SOLID acronym, they are:
[S] Single Responsibility Principle
There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.
[O] Open–Closed Principle
Software entities like classes, functions, etc should be open for extension, but closed for modification.
[L] Liskov Substitution Principle
Subclasses should be substitutable for their base classes.
[I] Interface Segregation Principle
This principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.
[D] Dependency Inversion Principle
High level modules should not depend on low level modules; both should depend on abstractions.
Let’s learn about these principles in detail with some examples in Javascript and Typescript.
The single responsibility principle states that every module, class or function in a computer program should have responsibility over a single part of program’s functionality.
If a class has many responsibilities, it increases the possibility of bugs as making change to any of the functionality may affect others without knowing.
Advantages
Disadvantages
Let’s take a look at the following code:
class TodoList {
constructor() {
this.items = []
}
addItem(item) {
this.items.push(item)
}
removeItem(index) {
this.items = items.splice(index, 1)
}
toString() {
return this.items.toString()
}
save(filename) {
fs.writeFileSync(filename, this.toString())
}
load(filename) {
fs.readFileSync(filename, {encoding:'utf8', flag:'r'})
}
} This class violates the Single responsibility principle. We added two functions save(filename) and load(filename) in TodoList class.
Let’s fix the code so that it complies with the “S” principle.
class TodoList {
constructor() {
this.items = []
}
addItem(item) {
this.items.push(item)
}
removeItem(index) {
this.items = items.splice(index, 1)
}
toString() {
return this.items.toString()
}
}
class DatabaseManager {
saveToFile(data, filename) {
fs.writeFileSync(filename, data.toString())
}
load(filename) {
fs.readFileSync(filename, {encoding:'utf8', flag:'r'})
}
} Thus our code has become more scalable. Of course, it may not look feasible when we look at small solutions. When applied to a complex architecture, this principle makes more meaning.
Objects or entities should be open for extension, but closed for modification.
Open for extension means that we should be able to add new features or components to the application without breaking existing code. Closed for modification means that we should not introduce breaking changes to existing functionality, because that would force us to refactor a lot of existing code.
Changing the current behaviour of a class will affect all the systems using that class. If we want the class to perform more functions, the ideal approach is to add to the functions that already exist not change them.
Let’s take a look at the following code:
class Person{
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 PersonFilter{
filterByName(persons, fullName) {
return persons.filter(person => person.fullName === fullName)
}
filterBySize(persons, language) {
return persons.filter(person => person.language === language)
}
filterByHobby(persons, hobby) {
return persons.filter(person => person.hobby === hobby)
}
} The problem with PersonFilter is that if we want to filter by any other new property we have to change PersonFilter’s code. Let's solve this problem by creating a filterByProp function which will filter by the properties.
class Person{
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 GenericFilter{
const filterByProp = (array, propName, value) =>
array.filter(element => element[propName] === value)
} If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
When a child Class cannot perform the same actions as its parent Class, this can cause bugs.
If you have a Class and create another Class from it, it becomes a parent and the new Class becomes a child. The child Class should be able to do everything the parent Class can do. This process is called Inheritance.
Let’s take a look at the following code:
class Bird {
fly(){
//..
}
}
class Eagle extends Bird {
dive(){
//..
}
}
const eagle = new Eagle();
eagle.fly();
eagle.dive();
class Penguin extends Bird(){
//Problem: Can't fly!
} The problem is that Penguin can’t fly. So to solve this problem, we have to create separate class for different kind of birds. Let’s look at the following example to solve above problem.
class Bird {
layEgg () {}
}
class FlyingBird {
fly () {}
}
class SwimmingBird extends Bird {
swim () {}
}
class Eagle extends FlyingBird {}
class Penguin extends SwimmingBird {}
const penguin = new Penguin();
penguin.swim();
penguin.layEgg();
const eagle = new Eagle();
eagle.fly();
eagle.layEgg(); Clients should not be forced to depend upon interfaces that they do not use.
When a Class is required to perform actions that are not useful, it is wasteful and may produce unexpected bugs if the Class does not have the ability to perform those actions.
A Class should perform only actions that are needed to fulfil its role. Any other action should be removed completely or moved somewhere else if it might be used by another Class in the future.
Let’s take a look at the following code:
In the following example we have an interface for animals with 2 methods: walk and fly.
As you can see, the Dog class has to implement the method fly even though that class does not need it.
interface Animal {
walk(): void;
fly(): void;
}
class Dog implements Animal {
walk() {
console.log("Walking");
}
fly() {
throw new Error("Dogs cannot fly");
}
}
class Duck implements Animal {
walk() {
console.log("Walking");
}
fly() {
console.log("Flying");
}
} Following this principle we can split the Animal interface into multiple ones. This way, the Dog class will only have to implement the methods that it needs.
interface AnimalCanWalk {
walk(): void;
}
interface AnimalCanFly {
fly(): void;
}
class Dog implements AnimalCanWalk {
walk() {
console.log("Walking");
}
}
class Duck implements AnimalCanWalk, AnimalCanFly {
walk() {
console.log("Walking");
}
fly() {
console.log("Flying");
}
} High-level modules should not depend on low-level modules. Both should depend on the abstraction.
In plain terms, this principle states that your classes should depend upon interfaces or abstract classes instead of concrete classes and functions. This makes your classes open to extension, following the open-closed principle.
Let’s take a look at the following code:
In the following bad example we have the OrderService class that saves orders in a database. The OrderService class depends directly on the low level class MySQL database.
If in the future we wanted to change the database that we are using we would have to modify the OrderService class.
class OrderService {
database: MySQLDatabase;
// constructor
save(order: Order): void {
if (order.id === undefined) {
this.database.insert(order);
} else {
this.database.update(order);
}
}
}
class MySQLDatabase {
insert(order: Order) {
// insert
}
update(order: Order) {
// update
}
} We can improve this by creating an interface and make the OrderService class dependant of it. This way, we are inverting the dependency. Now the high level class depends on an abstraction instead of a low level class.
class OrderService {
database: Database;
// constructor
save(order: Order): void {
this.database.save(order);
}
}
interface Database {
save(order: Order): void;
}
class MySQLDatabase implements Database {
save(order: Order) {
if (order.id === undefined) {
// insert
} else {
// update
}
}
} Now we can add new databases without modifying the OrderService class.
So far, we have discussed five principles with examples in Javascript and Typescript. When you design your architecture, If you ask yourself “Am I violating SOLID principles” , I promise that the quality and scalability of your code will be much better.
Thank you so much for reading. I hope you have a better idea about this topic and you had as much fun reading this as I did writing it.
If you have any questions or suggestions, leave a comment.
Thank you for being a part of the In Plain English community! Before you go:
<hr><p>SOLID principles made easy — JavaScript & TypeScript was originally published in JavaScript in Plain English on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>