SOLID Principles in JavaScript: Theory and Practice from A to Z
SOLID is an acronym for five fundamental principles of object-oriented programming formulated by Robert Martin (Uncle Bob). Although JavaScript is not a classical OOP language (it uses prototypal inheritance), these principles are applicable to it as well, especially in the context of Node.js and modern frameworks. Adhering to SOLID makes code more flexible, testable, and maintainable. In this article, we will break down each principle with practical examples in JavaScript.
1. S — Single Responsibility Principle
A class or module should have only one reason to change. In other words, each entity should be responsible for one specific task.
Bad example: The User class simultaneously manages user data and sends email notifications.
class User { constructor(name, email) { this.name = name; this.email = email; }
saveToDatabase() { // Logic for saving to DB console.log(`Saving user ${this.name} to DB`); }
sendWelcomeEmail() { // Logic for sending email console.log(`Sending welcome to ${this.email}`); }}Good example: Split the responsibility into two classes.
class User { constructor(name, email) { this.name = name; this.email = email; }}
class UserRepository { save(user) { console.log(`Saving user ${user.name} to DB`); }}
class EmailService { sendWelcome(user) { console.log(`Sending welcome to ${user.email}`); }}
// Usageconst user = new User('Ivan', 'ivan@example.com');const repo = new UserRepository();const emailService = new EmailService();
repo.save(user);emailService.sendWelcome(user);2. O — Open-Closed Principle
Software entities should be open for extension but closed for modification. This means new functionality should be added through new code, not by changing existing code.
Bad example: The AreaCalculator class requires modification when adding a new shape.
class AreaCalculator { calculate(shape) { if (shape.type === 'circle') { return Math.PI * shape.radius ** 2; } else if (shape.type === 'rectangle') { return shape.width * shape.height; } // When adding a new shape, this method needs to be changed }}Good example: Use polymorphism and inheritance.
class Shape { area() { throw new Error('The area() method must be overridden'); }}
class Circle extends Shape { constructor(radius) { super(); this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }}
class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; }
area() { return this.width * this.height; }}
class AreaCalculator { calculate(shape) { return shape.area(); }}
// Usageconst calculator = new AreaCalculator();const circle = new Circle(5);const rectangle = new Rectangle(4, 6);
console.log(calculator.calculate(circle)); // 78.54console.log(calculator.calculate(rectangle)); // 243. L — Liskov Substitution Principle
Objects in a program should be replaceable with instances of their sub