Introduction to SOLID Principles in JavaScript
SOLID is an acronym for five fundamental principles of object-oriented programming, formulated by Robert Martin (Uncle Bob). Despite the fact that JavaScript was not originally a strictly typed OOP language, modern JavaScript (ES6+) with classes, modules, and TypeScript actively uses these principles to create scalable and maintainable applications, especially in the Node.js environment.
In this article, we will analyze each of the five SOLID principles using practical code examples in JavaScript. Understanding these principles will help you write clean, flexible, and change-resistant code.
1. Single Responsibility Principle (SRP)
The principle states: a class or module should have only one reason to change. In other words, each module is responsible for one specific functionality.
Violation of SRP
Consider the UserService class, which simultaneously manages users, sends emails, and logs actions:
class UserService { constructor() { this.users = []; }
addUser(name) { this.users.push({ name }); console.log('User added'); // Logging this.sendEmail(name); // Sending email }
sendEmail(name) { console.log(`Email sent to ${name}`); }
getUsers() { return this.users; }}Here the class has three responsibilities: data management, logging, and sending notifications. Changing the logging logic or email sending will lead to modifying the UserService class.
Adhering to SRP
Let's separate the responsibilities into individual classes:
class Logger { log(message) { console.log(`[LOG]: ${message}`); }}
class EmailService { send(name) { console.log(`Email sent to ${name}`); }}
class UserService { constructor(logger, emailService) { this.users = []; this.logger = logger; this.emailService = emailService; }
addUser(name) { this.users.push({ name }); this.logger.log(`User ${name} added`); this.emailService.send(name); }
getUsers() { return this.users; }}Now each class is responsible for its own task, and changes in one service do not affect the others.
2. Open-Closed Principle (OCP)
The principle states: software entities should be open for extension but closed for modification. That is, we should be able to add new functionality without changing existing code.
Violation of OCP
Suppose we have a function for calculating the area of shapes:
function calculateArea(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 function would have to be modified}Adhering to OCP
We use polymorphism through classes:
class Shape { area() { throw new Error('Method "area()" must be implemented'); }}
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; }}
// Adding a new shape without modifying existing codeclass Triangle extends Shape { constructor(base, height) { super(); this.base = base; this.height = height; } area() { return (this.base * this.height) / 2; }}
function calculateArea