SOLID принципы в JavaScript: теория и практика от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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)

Определение: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько

Блоги

Рекомендаций по книгам