SOLID принципы в JavaScript: теория и практика для Node.js

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

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

Начать курс

SOLID принципы в JavaScript: от теории к практике на Node.js

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

Принцип единственной ответственности (Single Responsibility Principle, SRP)

SRP гласит: «У класса (или модуля) должна быть только одна причина для изменения». Это означает, что каждый модуль отвечает за одну конкретную задачу.

Плохой пример (нарушение SRP):

class UserService {
    createUser(data) { /* логика создания */ }
    sendWelcomeEmail(email) { /* отправка письма */ }
    logActivity(action) { /* логирование */ }
}

Здесь один класс отвечает за работу с пользователями, отправку писем и логирование. Если изменится формат логов, придется менять UserService — хотя его основная задача не менялась.

Хороший пример (соблюдение SRP):

class UserRepository {
    createUser(data) { /* только работа с БД */ }
}

class EmailService {
    sendWelcomeEmail(email) { /* только отправка */ }
}

class Logger {
    logActivity(action) { /* только логирование */ }
}

class UserService {
    constructor(userRepo, emailService, logger) {
        this.userRepo = userRepo;
        this.emailService = emailService;
        this.logger = logger;
    }
    createUser(data) {
        const user = this.userRepo.createUser(data);
        this.emailService.sendWelcomeEmail(user.email);
        this.logger.logActivity('User created');
        return user;
    }
}

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

Принцип открытости/закрытости (Open/Closed Principle, OCP)

OCP утверждает: «Программные сущности должны быть открыты для расширения, но закрыты для модификации». То есть мы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

Плохой пример (нарушение OCP):

class PaymentProcessor {
    processPayment(type, amount) {
        if (type === 'card') { /* обработка карты */ }
        else if (type === 'paypal') { /* обработка PayPal */ }
        // При добавлении нового типа — нужно менять этот метод
    }
}

Хороший пример (соблюдение OCP):

class PaymentStrategy {
    pay(amount) { throw new Error('Must be implemented'); }
}

class CardPayment extends PaymentStrategy {
    pay(amount) { console.log(`Paid ${amount} via Card`); }
}

class PayPalPayment extends PaymentStrategy {
    pay(amount) { console.log(`Paid ${amount} via PayPal`); }
}

class PaymentProcessor {
    constructor(strategy) {
        this.strategy = strategy;
    }
    processPayment(amount) {
        this.strategy.pay(amount);
    }
}

// Использование
const cardPayment = new PaymentProcessor(new CardPayment());
cardPayment.processPayment(100);

const paypalPayment = new PaymentProcessor(new PayPalPayment());
paypalPayment.processPayment(200);

Теперь для добавления нового способа оплаты достаточно создать новый класс, унаследованный от PaymentStrategy, и передать его в PaymentProcessor. Исходный код не меняется.

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP)

LSP гласит: «Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения правильности выполнения программы». Другими словами, если у вас есть базовый класс, то любой его подкласс должен корректно работать везде, где используется базовый класс.

Плохой пример (нарушение LSP):

class Rectangle {
    setWidth(w) { this.width = w; }
    setHeight(h) { this.height = h; }
    getArea() { return this.width * this.height; }
}

class Square extends Rectangle {
    setWidth(w) { this.width = w; this.height = w; }
    setHeight(h) { this.width = h; this.height = h; }
}

function printArea(rect) {
    rect.setWidth(5);
    rect.setHeight(10);
    console.log(rect.getArea()); // Ожидаем 50, но для Square получим 100
}

Здесь Square нарушает поведение Rectangle, так как изменяет обе стороны при установке одной. Это приводит к неожиданным результатам.

Хороший пример (соблюдение LSP):

class Shape {
    getArea() { throw new Error('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; }
}

function printArea(shape) {
    console.log(shape.getArea()); // Работает корректно для обеих фигур
}

Теперь оба класса наследуются от абстрактного Shape и предоставляют свою реализацию getArea(). Они взаимозаменяемы без нарушения логики.

Принцип разделения интерфейса (Interface Segregation Principle, ISP)

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('Robot does not eat'); }
    sleep() { throw new Error('Robot does not sleep'); }
}

Робот вынужден реализовывать методы eat и sleep, которые ему не нужны.

Хороший пример (соблюдение ISP):

class Workable {
    work() { throw new Error('Must be implemented'); }
}

class Eatable {
    eat() { throw new Error('Must be implemented'); }
}

class Sleepable {
    sleep() { throw new Error('Must be implemented'); }
}

class Human extends Workable {
    work() { console.log('Working'); }
}
// Human также может реализовать Eatable и Sleepable

class Robot extends Workable {
    work() { console.log('Working'); }
    // Robot не реализует Eatable и Sleepable
}

Разделив интер

Блоги

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