Введение в SOLID принципы в JavaScript
SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом). Несмотря на то, что JavaScript изначально не был строго типизированным ООП-языком, современный JavaScript (ES6+) с классами, модулями и TypeScript активно использует эти принципы для создания масштабируемых и поддерживаемых приложений, особенно в среде Node.js.
В этой статье мы разберем каждый из пяти принципов SOLID на практических примерах кода на JavaScript. Понимание этих принципов поможет вам писать чистый, гибкий и устойчивый к изменениям код.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Принцип гласит: у класса или модуля должна быть только одна причина для изменения. Другими словами, каждый модуль отвечает за одну конкретную функциональность.
Нарушение SRP
Рассмотрим класс UserService, который одновременно управляет пользователями, отправляет email и логирует действия:
class UserService {
constructor() {
this.users = [];
}
addUser(name) {
this.users.push({ name });
console.log('User added'); // Логирование
this.sendEmail(name); // Отправка email
}
sendEmail(name) {
console.log(`Email sent to ${name}`);
}
getUsers() {
return this.users;
}
}
Здесь класс имеет три обязанности: управление данными, логирование и отправка уведомлений. Изменение логики логирования или отправки email приведет к изменению класса UserService.
Соблюдение SRP
Разделим обязанности на отдельные классы:
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;
}
}
Теперь каждый класс отвечает за свою задачу, и изменения в одном сервисе не затрагивают другие.
2. Open-Closed Principle (OCP) — Принцип открытости/закрытости
Принцип гласит: программные сущности должны быть открыты для расширения, но закрыты для модификации. То есть мы должны иметь возможность добавлять новую функциональность, не изменяя существующий код.
Нарушение OCP
Предположим, у нас есть функция расчета площади фигур:
function calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
// При добавлении новой фигуры придется модифицировать эту функцию
}
Соблюдение OCP
Используем полиморфизм через классы:
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;
}
}
// Добавляем новую фигуру без изменения существующего кода
class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}
area() {
return (this.base * this.height) / 2;
}
}
function calculateArea(shape) {
return shape.area();
}
Теперь добавление новой фигуры не требует изменения функции calculateArea.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Принцип гласит: объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности программы. Проще говоря, если у вас есть базовый класс, то любой его подкласс должен корректно работать везде, где используется базовый класс.
Нарушение LSP
Классический пример с прямоугольником и квадратом:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // Нарушение: изменяем высоту
}
setHeight(height) {
this.height = height;
this.width = height; // Нарушение: изменяем ширину
}
}
function increaseRectangleWidth(rectangle) {
rectangle.setWidth(rectangle.width + 1);
return rectangle.getArea();
}
// Для квадрата это сломает логику
const square = new Square(5, 5);
console.log(increaseRectangleWidth(square)); // Ожидаем 30, получаем 36
Соблюдение LSP
Лучше сделать классы независимыми или использовать общий интерфейс:
class Shape {
getArea() {
throw new Error('Method "getArea()" must be implemented');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
Теперь и прямоугольник, и квадрат корректно заменяют базовый класс Shape.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса
Принцип гласит: клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше создавать множество специализированных интерфейсов, чем один универсальный.
В JavaScript нет строгих интерфейсов, но принцип применяется через композицию и проверку методов.
Нарушение ISP
class Worker {
work() { console.log('Working...'); }
eat() { console.log('Eating...'); }
sleep() { console.log('Sleeping...'); }
}
class Robot extends Worker {
eat() { throw new Error('Rob