SOLID принципы в JavaScript: от теории к практике с примерами кода

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

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

Начать курс

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

SOLID — это аббревиатура пяти фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом). Хотя JavaScript не является классическим ООП-языком (он использует прототипное наследование), эти принципы применимы и к нему, особенно в контексте Node.js и современных фреймворков. Соблюдение SOLID делает код более гибким, тестируемым и поддерживаемым. В этой статье мы разберем каждый принцип с практическими примерами на JavaScript.

1. S — Single Responsibility Principle (Принцип единственной ответственности)

Класс или модуль должен иметь только одну причину для изменения. Другими словами, каждая сущность должна отвечать за одну конкретную задачу.

Плохой пример: Класс User одновременно управляет данными пользователя и отправляет email-уведомления.

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    saveToDatabase() {
        // Логика сохранения в БД
        console.log(`Сохранение пользователя ${this.name} в БД`);
    }

    sendWelcomeEmail() {
        // Логика отправки email
        console.log(`Отправка приветствия на ${this.email}`);
    }
}

Хороший пример: Разделяем ответственность на два класса.

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

class UserRepository {
    save(user) {
        console.log(`Сохранение пользователя ${user.name} в БД`);
    }
}

class EmailService {
    sendWelcome(user) {
        console.log(`Отправка приветствия на ${user.email}`);
    }
}

// Использование
const user = new User('Иван', 'ivan@example.com');
const repo = new UserRepository();
const emailService = new EmailService();

repo.save(user);
emailService.sendWelcome(user);

2. O — Open-Closed Principle (Принцип открытости/закрытости)

Программные сущности должны быть открыты для расширения, но закрыты для модификации. Это означает, что новую функциональность следует добавлять через новый код, а не через изменение существующего.

Плохой пример: Класс AreaCalculator требует модификации при добавлении новой фигуры.

class AreaCalculator {
    calculate(shape) {
        if (shape.type === 'circle') {
            return Math.PI * shape.radius ** 2;
        } else if (shape.type === 'rectangle') {
            return shape.width * shape.height;
        }
        // При добавлении новой фигуры нужно менять этот метод
    }
}

Хороший пример: Используем полиморфизм и наследование.

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 AreaCalculator {
    calculate(shape) {
        return shape.area();
    }
}

// Использование
const calculator = new AreaCalculator();
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);

console.log(calculator.calculate(circle));    // 78.54
console.log(calculator.calculate(rectangle)); // 24

3. L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

Объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности программы. Простыми словами: если у вас есть функция, которая принимает базовый класс, она должна работать и с любым его наследником.

Плохой пример: Подкласс Square нарушает поведение родительского класса Rectangle.

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    setWidth(width) {
        this.width = width;
    }

    setHeight(height) {
        this.height = height;
    }

    area() {
        return this.width * this.height;
    }
}

class Square extends Rectangle {
    setWidth(width) {
        this.width = width;
        this.height = width; // Нарушение LSP: изменение ширины меняет высоту
    }

    setHeight(height) {
        this.width = height; // Нарушение LSP
        this.height = height;
    }
}

// Функция, которая ожидает Rectangle
function resizeRectangle(rect) {
    rect.setWidth(5);
    rect.setHeight(10);
    console.log(`Ожидаемая площадь: 50, фактическая: ${rect.area()}`);
}

const rect = new Rectangle(2, 3);
resizeRectangle(rect); // 50 - корректно

const square = new Square(4);
resizeRectangle(square); // 100 - некорректно

Решение: Избегайте наследования, которое меняет поведение. Лучше сделать Square отдельным классом или использовать композицию.

class Square {
    constructor(side) {
        this.side = side;
    }

    area() {
        return this.side ** 2;
    }
}

4. I — Interface Segregation Principle (Принцип разделения интерфейса)

Клиенты не должны зависеть от методов, которые они не используют. Вместо одного большого интерфейса лучше создать несколько маленьких и специализированных.

Плохой пример: Огромный интерфейс Worker.

class Worker {
    work() { /* ... */ }
    eat() { /* ... */ }
    sleep() { /* ... */ }
}

class Human extends Worker {
    work() { console.log('Человек работает'); }
    eat() { console.log('Человек ест'); }
    sleep() { console.log('Человек спит'); }
}

class Robot extends Worker {
    work() { console.log('Робот работает'); }
    // Роботу не нужны методы eat() и sleep(), но он вынужден их реализовывать
    eat() { throw new Error('Роботы не едят'); }
    sleep() { throw new Error('Роботы не спят'); }
}

Хороший пример: Разделяем интерфейсы.

class Workable {
    work() { throw new Error('Должен быть реализован'); }
}

class Eatable {
    eat() { throw new Error('Должен быть реализован'); }
}

class Sleepable {
    sleep() { throw new Error('Должен быть реализован'); }
}

class Human {
    constructor() {
        Object.assign(this, new Workable(), new Eatable(), new Sleepable());
    }
}

Human.prototype.work = function()                

Блоги

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