SOLID
SOLID — пять принципов объектно-ориентированного проектирования, которые помогают создавать гибкие, понятные и поддерживаемые системы.
Зачем нужно
SOLID-принципы решают главную проблему ООП — неуправляемую сложность. Без них классы превращаются в «божественные объекты», изменение в одном месте ломает десятки других, а тестирование становится кошмаром.
Где используется
- Проектирование классов и модулей
- Архитектура фронтенд-приложений (React-компоненты)
- Серверная архитектура (Express middleware, сервисы)
- Любой ООП-код
Предпосылки
S — Single Responsibility Principle (Принцип единственной ответственности)
Класс должен иметь одну и только одну причину для изменения.
// ПЛОХО: класс делает всё
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save {
// сохранение в БД
db.query('INSERT INTO users ...', [this.name, this.email]);
}
sendWelcomeEmail {
// отправка email
mailer.send(this.email, 'Добро пожаловать!');
}
generateReport {
// генерация отчёта
return `Отчёт по пользователю ${this.name}`;
}
}
// ХОРОШО: каждый класс — одна ответственность
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) {
db.query('INSERT INTO users ...', [user.name, user.email]);
}
findById(id) {
return db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
class EmailService {
sendWelcome(user) {
mailer.send(user.email, 'Добро пожаловать!');
}
}
class UserReportGenerator {
generate(user) {
return `Отчёт по пользователю ${user.name}`;
}
}
O — Open/Closed Principle (Принцип открытости/закрытости)
Сущности должны быть открыты для расширения, но закрыты для модификации.
// ПЛОХО: для нового типа скидки нужно менять существующий код
function calculateDiscount(order, type) {
if (type === 'percentage') {
return order.total * (order.discountValue / 100);
} else if (type === 'fixed') {
return order.discountValue;
} else if (type === 'bogo') { // каждый раз добавляем else if
return order.total / 2;
}
}
// ХОРОШО: новые скидки — новые классы, старый код не меняется
class PercentageDiscount {
constructor(percent) { this.percent = percent; }
calculate(total) { return total * (this.percent / 100); }
}
class FixedDiscount {
constructor(amount) { this.amount = amount; }
calculate(total) { return Math.min(this.amount, total); }
}
class BogoDiscount {
calculate(total) { return total / 2; }
}
// Новая скидка — просто новый класс, ничего не ломается
class LoyaltyDiscount {
constructor(years) { this.years = years; }
calculate(total) { return total * Math.min(this.years * 0.02, 0.2); }
}
function applyDiscount(order, discount) {
return order.total - discount.calculate(order.total);
}
L — Liskov Substitution Principle (Принцип подстановки Лисков)
Объекты дочернего класса должны быть заменяемы объектами родительского класса без нарушения работы программы.
// ПЛОХО: Square нарушает поведение Rectangle
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area { 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;
}
}
// Код, ожидающий Rectangle, сломается с Square
function doubleWidth(rect) {
const oldHeight = rect.height;
rect.setWidth(rect.width * 2);
// Ожидаем: area = width*2 * oldHeight
// С Square: area = (width*2)^2 — НЕВЕРНО
console.assert(rect.height === oldHeight); // Fails for Square!
}
// ХОРОШО: общий интерфейс без наследования, нарушающего контракт
class Shape {
area { throw new Error('Not implemented'); }
}
class Rectangle extends Shape {
constructor(width, height) {
super;
this.width = width;
this.height = height;
}
area { return this.width * this.height; }
}
class Square extends Shape {
constructor(side) {
super;
this.side = side;
}
area { return this.side ** 2; }
}
I — Interface Segregation Principle (Принцип разделения интерфейсов)
Клиент не должен зависеть от методов, которые он не использует.
// ПЛОХО: один гигантский "интерфейс" для всех
class Animal {
fly { throw new Error('Не умею летать'); }
swim { throw new Error('Не умею плавать'); }
run { throw new Error('Не умею бегать'); }
speak { throw new Error('Не умею говорить'); }
}
class Dog extends Animal {
run { return 'Бегу!'; }
swim { return 'Плыву!'; }
speak { return 'Гав!'; }
// fly — бросает ошибку, но собака знает о методе fly
}
// ХОРОШО: маленькие миксины (в JS нет интерфейсов, но есть композиция)
const canRun = {
run { return `${this.name} бежит`; },
};
const canSwim = {
swim { return `${this.name} плывёт`; },
};
const canFly = {
fly { return `${this.name} летит`; },
};
class Dog {
constructor(name) { this.name = name; }
}
Object.assign(Dog.prototype, canRun, canSwim);
class Eagle {
constructor(name) { this.name = name; }
}
Object.assign(Eagle.prototype, canFly);
const dog = new Dog('Рекс');
dog.run; // "Рекс бежит"
dog.swim; // "Рекс плывёт"
// dog.fly — undefined, не бросает ошибку
D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
// ПЛОХО: бизнес-логика напрямую зависит от конкретной БД
class UserService {
constructor {
// жёсткая привязка к MySQL
this.db = new MySQLDatabase('localhost', 'users_db');
}
getUser(id) {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// ХОРОШО: зависимость передаётся извне (Dependency Injection)
class UserService {
constructor(database) {
this.db = database; // любой объект с методом findById
}
getUser(id) {
return this.db.findById('users', id);
}
}
// В продакшене
const service = new UserService(new PostgresDatabase(config));
// В тестах
const service = new UserService(new InMemoryDatabase);
// При миграции на MongoDB — меняем только конфигурацию
const service = new UserService(new MongoDatabase(config));
Dependency Injection в Express
// Инверсия зависимостей через фабрику роутов
function createUserRouter(userService, emailService) {
const router = express.Router;
router.post('/users', async (req, res) => {
const user = await userService.create(req.body);
await emailService.sendWelcome(user);
res.status(201).json(user);
});
return router;
}
// Сборка приложения — единственное место, где создаются зависимости
const userService = new UserService(new PostgresDatabase(config));
const emailService = new EmailService(new SMTPTransport(smtpConfig));
app.use('/api', createUserRouter(userService, emailService));
Сводная таблица
| Принцип | Что делать | Что не делать |
|---|---|---|
| SRP | Один класс = одна задача | God Object |
| OCP | Расширять через новые классы | Править if/switch при каждой фиче |
| LSP | Подклассы заменяемы | Переопределять с побочными эффектами |
| ISP | Маленькие интерфейсы | Гигантский базовый класс |
| DIP | Внедрять зависимости | Создавать зависимости внутри класса |
Частые ошибки
- Применять SOLID к каждому классу. Для простых утилит и скриптов SOLID — overkill
- Создавать интерфейс ради интерфейса. Если реализация одна — абстракция не нужна
- Путать SRP с «один метод на класс». SRP — про одну причину изменения, не про размер
- Забывать про тестируемость. SOLID улучшает тестируемость — если код нельзя протестировать, скорее всего нарушен DIP
Практика
- Возьмите один «большой» класс в проекте и разбейте по SRP
- Замените
if/switchс типами на полиморфизм (OCP) - Внедрите хотя бы одну зависимость через конструктор вместо создания внутри класса (DIP)
🎓 Источники
- 🎓 [SOLID принципы для JavaScript, TypeScript, Node.js] · 2024-06-08 · YouTube
- Тезисы: автор расширяет SOLID на мультипарадигменный JS — на процедурное, функциональное, реактивное программирование. TypeScript не проверяет соответствие SOLID. SOLID не автоматизируется — нужен другой программист на review. Active Record ломает весь SOLID разом. GPT-4 для рефакторинга по SOLID работает плохо, потому что обучался на массе неоптимального JS-кода.
- Альтернативная позиция: «Ларавель имеет другую идеологию абсолютно. Он абсолютно отвергает SOLID. Active Record против SOLID». Frameworks вроде Laravel/Rails концептуально несовместимы с SOLID.
- 🎓 [Dependency Inversion Principle for JavaScript] · 2024-11-23 · YouTube
- Тезисы: DIP — самый сложный, недопонятый и самый полезный из SOLID. Открывает путь к архитектуре. DIP мультипарадигменный — работает в ООП, процедурном, ФП, реактивном. Service Locator и DI — частные подчинённые DIP механизмы.
- «Модули верхних уровней не должны зависеть от модулей нижних. Оба должны зависеть от интерфейсов. Абстракции не должны зависеть от деталей, а детали — от абстракций».
Связанные темы
- DRY
- KISS
- YAGNI
- Clean Code
- Dependency Injection
- Layered Architecture
- SOLID — позиция Тимура — провокативный контр-консенсус, Active Record vs SOLID
- ФП и SOLID GRASP GoF — FP-аналоги принципов
Ресурсы
- Robert C. Martin — Clean Architecture
- refactoring.guru/design-patterns — паттерны проектирования
- wikipedia.org — SOLID