Антон Ларичев

Введение
SOLID — это пять принципов объектно-ориентированного проектирования, которые помогают создавать код, устойчивый к изменениям. Аббревиатура расшифровывается: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Несмотря на то что термины пришли из мира Java и C#, в JavaScript они применяются не менее активно — особенно в Node.js-приложениях и фронтенд-фреймворках.
В этой статье мы разберём каждый принцип на конкретных примерах и покажем, как нарушение принципов приводит к проблемам на практике.
S — Single Responsibility Principle (Принцип единственной ответственности)
Класс или функция должны выполнять только одну задачу и иметь только одну причину для изменения.
Нарушение принципа
class UserService {
// Класс делает слишком много: бизнес-логика, валидация и работа с БД вместе
createUser(data) {
if (!data.email.includes('@')) {
throw new Error('Неверный email');
}
const user = { ...data, createdAt: new Date() };
db.query('INSERT INTO users VALUES (?)', [user]);
emailClient.send(data.email, 'Добро пожаловать!');
return user;
}
}
Соблюдение принципа
class UserValidator {
// Отвечает только за валидацию
validate(data) {
if (!data.email.includes('@')) {
throw new Error('Неверный email');
}
}
}
class UserRepository {
// Отвечает только за сохранение
save(user) {
return db.query('INSERT INTO users VALUES (?)', [user]);
}
}
class UserService {
constructor(validator, repository, mailer) {
this.validator = validator;
this.repository = repository;
this.mailer = mailer;
}
createUser(data) {
this.validator.validate(data);
const user = { ...data, createdAt: new Date() };
this.repository.save(user);
this.mailer.sendWelcome(data.email);
return user;
}
}
Теперь каждый класс можно изменить и протестировать независимо.
O — Open/Closed Principle (Принцип открытости/закрытости)
Модуль должен быть открыт для расширения, но закрыт для модификации.
// Плохо: при добавлении нового типа скидки придётся менять этот класс
class DiscountService {
calculate(user, price) {
if (user.type === 'vip') return price * 0.8;
if (user.type === 'student') return price * 0.9;
return price;
}
}
// Хорошо: новые скидки добавляются без изменения существующего кода
class VipDiscount {
apply(price) { return price * 0.8; }
}
class StudentDiscount {
apply(price) { return price * 0.9; }
}
class NoDiscount {
apply(price) { return price; }
}
class DiscountService {
constructor(discountStrategy) {
// Принимаем стратегию снаружи — расширяем через новые классы
this.strategy = discountStrategy;
}
calculate(price) {
return this.strategy.apply(price);
}
}
L — Liskov Substitution Principle (Принцип подстановки Лисков)
Дочерний класс должен полностью заменять родительский без изменения поведения программы.
class Bird {
fly() { return 'Лечу'; }
}
// Нарушение: пингвин — птица, но не умеет летать
class Penguin extends Bird {
fly() {
throw new Error('Пингвины не летают');
}
}
// Решение: выделить поведение в отдельные интерфейсы
class Bird {
move() { throw new Error('Метод не реализован'); }
}
class FlyingBird extends Bird {
move() { return 'Лечу'; }
}
class SwimmingBird extends Bird {
move() { return 'Плыву'; }
}
class Sparrow extends FlyingBird {}
class Penguin extends SwimmingBird {}
// Теперь любую птицу можно передать в функцию без неожиданных ошибок
function makeMove(bird) {
console.log(bird.move());
}
I — Interface Segregation Principle (Принцип разделения интерфейса)
Клиенты не должны зависеть от методов, которые они не используют. В JavaScript интерфейсов нет, но принцип применяется через разделение объектов и миксинов.
// Плохо: один большой объект со всеми методами
class Animal {
eat() {}
sleep() {}
fly() {} // не все животные умеют летать
swim() {} // не все умеют плавать
}
// Хорошо: разбиваем на небольшие роли-миксины
const canEat = (Base) => class extends Base {
eat() { return `${this.name} ест`; }
};
const canFly = (Base) => class extends Base {
fly() { return `${this.name} летит`; }
};
const canSwim = (Base) => class extends Base {
swim() { return `${this.name} плывёт`; }
};
class Animal {
constructor(name) { this.name = name; }
}
// Составляем только нужные возможности
class Duck extends canEat(canFly(canSwim(Animal))) {}
class Cat extends canEat(Animal) {}
const duck = new Duck('Утка');
console.log(duck.fly()); // Утка летит
console.log(duck.swim()); // Утка плывёт
D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
// Плохо: высокоуровневый класс жёстко привязан к конкретной реализации
class OrderService {
constructor() {
// Прямая зависимость от конкретного класса — не заменить на другой логгер
this.logger = new FileLogger();
}
createOrder(data) {
this.logger.log('Создан заказ');
return data;
}
}
// Хорошо: зависим от абстракции, конкретику передаём снаружи
class OrderService {
constructor(logger) {
// Принимаем любой объект с методом log
this.logger = logger;
}
createOrder(data) {
this.logger.log('Создан заказ');
return data;
}
}
class FileLogger {
log(msg) { fs.appendFileSync('app.log', msg); }
}
class ConsoleLogger {
log(msg) { console.log(msg); }
}
// В продакшене используем файловый логгер, в тестах — консольный
const service = new OrderService(new FileLogger());
const testService = new OrderService(new ConsoleLogger());
Частые ошибки
Слишком формальное следование принципам. Создавать 10 классов там, где хватит одной функции — это оверинжиниринг. SOLID — инструмент, а не религия. Применяйте принципы там, где код реально будет меняться и расширяться.
Путаница между SRP и дроблением. Единственная ответственность не означает «один метод на класс». Ответственность — это причина для изменения. Если класс хранит данные пользователя и умеет их сериализовать — у него всё ещё одна область изменений.
Игнорирование LSP при работе с промисами. Если метод базового класса возвращает синхронное значение, а дочерний класс возвращает промис — это нарушение LSP. Держите контракты совместимыми.
Dependency Injection без IoC-контейнера. Передавать зависимости через конструктор — это уже DI. Не нужно сразу внедрять тяжёлые фреймворки для маленьких проектов.
Заключение
SOLID принципы в JavaScript помогают писать код, который легко тестировать, расширять и передавать другим разработчикам. Начните с SRP и DIP — они дают наибольший эффект сразу. Остальные принципы приходят естественно, когда вы начинаете мыслить в терминах ответственности и зависимостей. Главное — не применять принципы механически, а понимать, какую проблему каждый из них решает.






Комментарии
0