Перейти к содержанию
Innovialabs

Принципы SOLID: объяснение на практических примерах

Практика4 мин

Коротко

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Пустые методы-заглушкиРазделить интерфейс
DIPnew инфраструктуры внутри бизнес-логикиВнедрять через интерфейс

Частые ошибки

  • Применять SOLID слишком рано: три интерфейса для функции с одной реализацией и без планов на изменения только добавляют шум.
  • Считать это законом: это ориентиры для часто меняющегося кода, а не чек-лист для каждого файла.
  • Дробить слишком мелко: десятки классов по одной строке читать так же тяжело, как один гигантский.

Практичное правило: рефакторьте в сторону SOLID тогда, когда изменение причиняет боль, — на второй-третий раз правки одного и того же места.

Частые вопросы

Идеи — да. В функциональном коде единственная ответственность означает маленькие узкие функции, а инверсия зависимостей — передачу зависимостей аргументами вместо прямого импорта.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.