SOLID принципы в JavaScript: теория и практика от А до Я
Разработка качественного программного обеспечения требует не только знания синтаксиса языка, но и понимания принципов архитектуры. SOLID — это пять фундаментальных принципов объектно-ориентированного проектирования, которые помогают создавать гибкий, поддерживаемый и масштабируемый код. В этой статье мы разберем каждый принцип SOLID в контексте JavaScript (Node.js) с практическими примерами.
Что такое SOLID?
Акроним SOLID был введен Робертом Мартином (дядюшка Боб) и включает следующие принципы:
- S — Single Responsibility Principle (Принцип единственной ответственности)
- O — Open/Closed Principle (Принцип открытости/закрытости)
- L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
- I — Interface Segregation Principle (Принцип разделения интерфейса)
- D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Эти принципы универсальны и применимы к любому языку программирования, включая JavaScript. Рассмотрим каждый из них подробно.
1. Single Responsibility Principle (SRP)
Определение: Класс или модуль должен иметь только одну причину для изменения. Другими словами, каждый модуль отвечает за одну конкретную задачу.
Плохой пример: Класс UserService, который отвечает и за сохранение пользователя, и за отправку email, и за генерацию отчетов.
// ❌ Нарушение SRP
class UserService {
saveUser(user) {
// Сохранение в базу данных
console.log('User saved');
}
sendEmail(user) {
// Отправка email
console.log('Email sent to ' + user.email);
}
generateReport(user) {
// Генерация отчета
console.log('Report generated for ' + user.name);
}
}
Хороший пример: Разделяем ответственность на отдельные классы.
// ✅ Соблюдение SRP
class UserRepository {
save(user) {
console.log('User saved');
}
}
class EmailService {
send(user) {
console.log('Email sent to ' + user.email);
}
}
class ReportGenerator {
generate(user) {
console.log('Report generated for ' + user.name);
}
}
// Использование
const user = { name: 'Иван', email: 'ivan@example.com' };
const userRepo = new UserRepository();
const emailService = new EmailService();
const reportGen = new ReportGenerator();
userRepo.save(user);
emailService.send(user);
reportGen.generate(user);
Теперь каждый класс имеет одну ответственность, и изменения в одном модуле не затрагивают другие.
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;
}
// При добавлении новой фигуры приходится изменять функцию
return 0;
}
Хороший пример: Используем полиморфизм и наследование.
// ✅ Соблюдение OCP
class Shape {
area() {
throw new Error('Метод area() должен быть реализован');
}
}
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();
}
Теперь для добавления новой фигуры достаточно создать новый класс, не трогая существующий код.
3. Liskov Substitution Principle (LSP)
Определение: Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. Проще говоря, если у вас есть базовый класс, то его наследники должны полностью соответствовать его контракту.
Плохой пример: Класс Rectangle и его наследник Square, который нарушает поведение родителя.
// ❌ Нарушение 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 {
constructor(size) {
super(size, size);
}
setWidth(width) {
this.width = width;
this.height = width; // Нарушение: изменение высоты при установке ширины
}
setHeight(height) {
this.height = height;
this.width = height; // Нарушение: изменение ширины при установке высоты
}
}
// Клиентский код ожидает, что установка разных значений даст прямоугольник
function resizeRectangle(rect) {
rect.setWidth(5);
rect.setHeight(10);
console.log(rect.getArea()); // Для Rectangle: 50, для Square: 100 (неверно)
}
Хороший пример: Избегаем наследования, если поведение различается. Используем композицию или отдельные классы.
// ✅ Соблюдение 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 {
constructor(size) {
this.size = size;
}
setSize(size) {
this.size = size;
}
getArea() {
return this.size * this.size;
}
}
// Теперь каждый класс имеет свое поведение, и подстановка не нарушает логику
Лучше использовать композицию или отдельные иерархии, если поведение наследника принципиально отличается.
4. Interface Segregation Principle (ISP)
Определение: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько