Принципы SOLID: объяснение на практических примерах
Практика4 минРедакция Innovialabs
Коротко
SOLID — пять правил, которые делают код простым для изменений: одна причина для изменения класса, расширение без правки, безопасная подстановка, маленькие интерфейсы и зависимость от абстракций, а не от конкретных классов.
Что SOLID даёт на практике
SOLID — пять принципов проектирования объектно-ориентированного кода. Общая цель проста: изменение в одном месте не должно ломать другие места или требовать там правок. Ниже у каждого принципа есть пример «до» и «после» на TypeScript и конкретная польза.
S: принцип единственной ответственности
У класса должна быть одна причина для изменения.
// До: расчёт отчёта, форматирование и почта в одном классе
class Report {
build(data: Order[]) { /* подсчёт итогов */ }
toPdf() { /* рендер PDF */ }
sendByEmail(to: string) { /* SMTP */ }
}
// После: каждый класс меняется по своей причине
class ReportBuilder { build(data: Order[]): ReportData { /* ... */ } }
class PdfRenderer { render(r: ReportData): Buffer { /* ... */ } }
class Mailer { send(to: string, file: Buffer) { /* ... */ } }
Польза: правка макета PDF больше не рискует сломать подсчёт итогов, а каждую часть можно тестировать отдельно.
O: принцип открытости/закрытости
Код открыт для расширения и закрыт для изменения: новое поведение добавляется новым кодом, а не правкой работающего.
// До: каждый новый способ доставки — правка этой функции
function deliveryCost(type: string, weight: number) {
if (type === "courier") return 20 + weight * 2;
if (type === "pickup") return 0;
throw new Error("unknown type");
}
// После: новый тип доставки — новый класс
interface Delivery { cost(weight: number): number }
class Courier implements Delivery { cost(w: number) { return 20 + w * 2; } }
class Pickup implements Delivery { cost() { return 0; } }
Польза: новый тип доставки не трогает протестированный код, поэтому регрессий меньше.
L: принцип подстановки Лисков
Подкласс должен работать везде, где ожидается родитель, без сюрпризов.
// До: ReadOnlyFile нарушает контракт File
class File { write(text: string) { /* ... */ } }
class ReadOnlyFile extends File {
write() { throw new Error("read-only"); }
}
// После: возможности разделены, обещания не нарушаются
interface Readable { read(): string }
interface Writable { write(text: string): void }
class ReadOnlyFile implements Readable { read() { return "..."; } }
Польза: коду, который принимает родительский тип, не нужны проверки instanceof и try/catch для «особых» подклассов.
I: принцип разделения интерфейсов
Клиенты не должны зависеть от методов, которыми не пользуются.
// До: один «толстый» интерфейс
interface Worker { code(): void; test(): void; manageBudget(): void }
// После: маленькие узкие интерфейсы
interface Coder { code(): void }
interface Tester { test(): void }
interface BudgetOwner { manageBudget(): void }
Польза: классы реализуют только то, что им нужно, а изменение одного интерфейса затрагивает меньше файлов.
D: принцип инверсии зависимостей
Бизнес-логика должна зависеть от абстракций, а не от конкретных реализаций.
// До: сервис жёстко привязан к одной БД
class OrderService {
private db = new PostgresClient();
save(o: Order) { this.db.insert("orders", o); }
}
// После: зависимость передаётся через интерфейс
interface OrderRepository { save(o: Order): Promise<void> }
class OrderService {
constructor(private repo: OrderRepository) {}
place(o: Order) { return this.repo.save(o); }
}
Польза: в тестах передаётся репозиторий в памяти, а смена хранилища не затрагивает бизнес-логику.
Шпаргалка
| Принцип | Тревожный признак | Типичное решение |
|---|---|---|
| SRP | Класс меняется по нескольким несвязанным причинам | Разделить по ответственности |
| OCP | Растущий if/switch по типу | Полиморфизм или карта стратегий |
| LSP | Подкласс бросает «не поддерживается» | Пересмотреть иерархию |
| ISP | Пустые методы-заглушки | Разделить интерфейс |
| DIP | new инфраструктуры внутри бизнес-логики | Внедрять через интерфейс |
Частые ошибки
- Применять SOLID слишком рано: три интерфейса для функции с одной реализацией и без планов на изменения только добавляют шум.
- Считать это законом: это ориентиры для часто меняющегося кода, а не чек-лист для каждого файла.
- Дробить слишком мелко: десятки классов по одной строке читать так же тяжело, как один гигантский.
Практичное правило: рефакторьте в сторону SOLID тогда, когда изменение причиняет боль, — на второй-третий раз правки одного и того же места.