DevLearn logo
Skill Up With Me
Interactive Learning
Signing in…

Принципы SOLID: глубокое погружение

🔒 Sign in to use this

SOLID — это аббревиатура пяти принципов объектно-ориентированного проектирования, введённых Робертом Мартином в начале 2000-х. Каждый принцип адресует конкретный сбой проектирования — симптомы, которые вы узнаёте в реальных кодовых базах: классы, которые невозможно расширить, модули, ломающиеся в неожиданных местах при изменении, код, который нельзя протестировать изолированно. Принципы SOLID — это не правила для механического применения, а линзы для выявления проблем проектирования и направления рефакторинга. В этом уроке каждый принцип разобран точно: сначала нарушение, затем исправление, затем архитектурные последствия.

ℹ️Принципы SOLID — это средства, а не цели. Класс, нарушающий SRP, но занимающий 50 строк, не имеющий сотрудников и никогда не меняющийся — нормальный класс. Класс, следующий каждому принципу механически, но добавляющий три уровня косвенности к тривиальной операции — хуже, чем нарушение. Применяйте принципы там, где они решают реальную проблему.
S — Принцип единственной ответственности

Формулировка Мартина: «У класса должна быть только одна причина для изменения». Ключевое слово — «причина»: оно относится к актору или стейкхолдеру, чьи требования могут вызвать изменение, а не к синтаксическому правилу «один метод». Класс, который форматирует отчёты И сохраняет их в базу данных, имеет две причины для изменения: команда по отчётности (изменение формата) и команда эксплуатации (миграция базы данных). Когда эти изменения происходят одновременно, они конфликтуют. SRP говорит: эти задачи относятся к разным классам.

Нажмите на подсвеченные строки, чтобы понять нарушение SRP и исправление
java
1
// ── НАРУШЕНИЕ: две причины для изменения в одном классе ─────────
2
class OrderProcessor {
3
  void process(Order order) {
4
    // бизнес-логика
5
    order.setStatus(CONFIRMED);
6
    // сохранение — причина #2 для изменения
7
    db.save(order);
8
    // уведомление — причина #3 для изменения
9
    emailClient.send(order.getEmail(), buildTemplate(order));
10
  }
11
}
12
13
// ── ИСПРАВЛЕНИЕ: у каждого класса одна причина для изменения ────
14
class OrderConfirmationService {
15
  OrderConfirmationService(OrderRepository repo, OrderNotificationService notifier) { ... }
16
  void confirm(Order order) {
17
    order.confirm();          // только доменная логика
18
    repo.save(order);         // делегирует сохранение
19
    notifier.sendConfirmation(order); // делегирует уведомление
20
  }
21
}
22
class OrderRepository      { void save(Order o) { ... } }   // только БД
23
class OrderNotificationService { void sendConfirmation(Order o) { ... } } // только email
O — Принцип открытости/закрытости

Формулировка Бертрана Мейера (1988), переосмысленная Мартином: «Программные сущности должны быть открыты для расширения, но закрыты для модификации». Класс закрыт для модификации, когда его проверенный, задеплоенный код не нужно менять для добавления нового поведения. Он открыт для расширения, когда новое поведение добавляется написанием нового кода — нового подкласса, новой реализации стратегии, нового декоратора — без прикосновения к существующему классу. Самый распространённый механизм: зависеть от абстракций (интерфейсов) и внедрять конкретные реализации.

Нажмите на подсвеченные строки, чтобы увидеть нарушение и исправление OCP
java
1
// ── НАРУШЕНИЕ: каждый новый способ оплаты требует модификации ───
2
class PaymentProcessor {
3
  void process(Payment p) {
4
    if (p.type() == CREDIT_CARD) chargeCard(p);
5
    else if (p.type() == PAYPAL)  chargePayPal(p);
6
    else if (p.type() == CRYPTO)  chargeCrypto(p);  // добавлено позже — изменён существующий класс
7
  }
8
}
9
10
// ── ИСПРАВЛЕНИЕ: закрыт для модификации, открыт для расширения ──
11
interface PaymentGateway { void charge(Payment p); }
12
class CreditCardGateway  implements PaymentGateway { ... }
13
class PayPalGateway       implements PaymentGateway { ... }
14
class CryptoGateway       implements PaymentGateway { ... }  // новый метод = только новый класс
15
16
class PaymentProcessor {
17
  private final Map<PaymentType, PaymentGateway> gateways;
18
  void process(Payment p) {
19
    gateways.get(p.type()).charge(p);  // никогда не изменяется
20
  }
21
}
L — Принцип подстановки Барбары Лисков

Формулировка Барбары Лисков (1987): «Если S является подтипом T, то объекты типа T можно заменять объектами типа S, не нарушая желательных свойств программы». Простыми словами: подкласс должен соблюдать контракт суперкласса — не только сигнатуры методов, но и поведенческие ожидания (предусловия, постусловия, инварианты). Классическое нарушение LSP — проблема Квадрата-Прямоугольника: Square расширяет Rectangle, но переопределение setWidth с одновременным изменением height нарушает контракт прямоугольника о независимости ширины и высоты.

Нажмите на подсвеченные строки, чтобы понять нарушение LSP и правильный дизайн
java
1
// ── НАРУШЕНИЕ: Square нарушает поведенческий контракт Rectangle ─
2
class Rectangle {
3
  protected int width, height;
4
  void setWidth(int w)  { this.width = w; }
5
  void setHeight(int h) { this.height = h; }
6
  int area() { return width * height; }
7
}
8
class Square extends Rectangle {
9
  @Override void setWidth(int w)  { width = height = w; }  // нарушает контракт Rectangle
10
  @Override void setHeight(int h) { width = height = h; }  // нарушает контракт Rectangle
11
}
12
// Клиентский код ломается при подстановке Square вместо Rectangle:
13
// rect.setWidth(5); rect.setHeight(4); assert rect.area() == 20; // ПРОВАЛ для Square
14
15
// ── ИСПРАВЛЕНИЕ: моделируем реальное отношение — без наследования ─
16
interface Shape { int area(); }
17
record Rectangle(int width, int height) implements Shape {
18
  public int area() { return width * height; }
19
}
20
record Square(int side) implements Shape {
21
  public int area() { return side * side; }
22
}
I — Принцип разделения интерфейсов

Формулировка Мартина: «Клиенты не должны зависеть от интерфейсов, которые они не используют». Толстые интерфейсы — с множеством несвязанных методов — вынуждают реализаторов создавать заглушки для методов, которые им не нужны, а клиентов — импортировать тип, несущий возможности, которые им не нужны. ISP говорит: предпочитайте множество маленьких клиентских интерфейсов одному большому. Проектное давление ISP естественно порождает Ролевые Интерфейсы — интерфейсы, описывающие роль объекта (Printable, Saveable, Auditable), а не полный перечень всего, что объект умеет.

Нажмите на подсвеченные строки, чтобы понять нарушение ISP и ролевые интерфейсы
java
1
// ── НАРУШЕНИЕ: толстый интерфейс принуждает к ненужным заглушкам ─
2
interface Worker {
3
  void work();
4
  void takeBreak();      // нерелевантно для Robot
5
  void receivePaycheck(); // нерелевантно для Robot
6
}
7
class Robot implements Worker {
8
  public void work() { ... }
9
  public void takeBreak() { /* Robot не отдыхает — принудительная заглушка */ }
10
  public void receivePaycheck() { /* Robot не получает зарплату — заглушка */ }
11
}
12
13
// ── ИСПРАВЛЕНИЕ: маленькие ролевые интерфейсы ───────────────────
14
interface Workable  { void work(); }
15
interface Breakable { void takeBreak(); }
16
interface Payable   { void receivePaycheck(); }
17
18
class Robot      implements Workable { ... }                        // только нужное
19
class HumanWorker implements Workable, Breakable, Payable { ... }   // полный набор
20
21
// Планировщик нуждается только в Workable — не знает о перерывах и оплате:
22
class ProductionScheduler { void schedule(List<Workable> workers) { ... } }
D — Принцип инверсии зависимостей

Формулировка Мартина: «(A) Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. (B) Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций». DIP — это принцип, лежащий в основе Луковой архитектуры, Гексагональной архитектуры и Чистой архитектуры. Он же делает внедрение зависимостей осмысленным: DI — это механизм; DIP — это причина.

Нажмите на подсвеченные строки, чтобы проследить инверсию зависимостей от нарушения к исправлению
java
1
// ── НАРУШЕНИЕ: модуль верхнего уровня зависит от детали ─────────
2
class OrderService {
3
  private final MySqlOrderRepository repo = new MySqlOrderRepository();
4
  void confirm(Long id) {
5
    Order order = repo.findById(id);
6
    order.confirm();
7
    repo.save(order);
8
  }
9
}
10
11
// ── ИСПРАВЛЕНИЕ: оба зависят от абстракции ───────────────────────
12
interface OrderRepository { Order findById(Long id); void save(Order o); }
13
14
class OrderService {
15
  private final OrderRepository repo;  // зависит от абстракции
16
  OrderService(OrderRepository repo) { this.repo = repo; }  // DI через конструктор
17
  void confirm(Long id) { ... }  // логика не изменилась
18
}
19
20
// Низкоуровневая деталь зависит от абстракции (не наоборот):
21
class MySqlOrderRepository implements OrderRepository { ... }    // продакшен
22
class InMemoryOrderRepository implements OrderRepository { ... } // тесты
SOLID на практике: полная картина
Быстрый справочник: какую проблему решает каждый принцип и какой механизм предписывает
ПринципУстраняемый симптомМеханизмАрхитектурный эффект
SRPКлассы меняются по нескольким несвязанным причинам; конфликты слияния; разрозненные тестыРазделить по актору/причине измененияДвигает к модульной декомпозиции; независимый деплой
OCPДобавление фич требует модификации проверенного кода; растущие цепочки if/elseЗависеть от абстракций; расширять добавлением новых реализацийПлагинные архитектуры, паттерн Strategy, точки расширения
LSPПодклассы, ломающие вызывающий код; UnsupportedOperationException в переопределенияхСоблюдать поведенческий контракт, не только сигнатурыГарантирует подстановляемость в полиморфных иерархиях; композиция вместо наследования
ISPРеализация нерелевантных методов; толстые интерфейсы, часто меняющиесяМаленькие ролевые интерфейсы для каждого клиентаСнижает связанность между модулями
DIPВысокоуровневый код зависит от БД/фреймворка; не тестируется без инфраструктурыАбстракции в пакете высокого уровня; детали внедряютсяОснова Луковой/Гексагональной/Чистой архитектуры; тест-дублёры
Типичные ошибки при применении SOLID
Нажмите на карточку — механическое применение SOLID порождает собственные проблемы
🔪
SRP в крайности
🔮
Преждевременные абстракции OCP
🌀
Взрыв интерфейсов из-за DIP
Главный вывод: принципы SOLID адресуют пять конкретных сбоев объектно-ориентированного проектирования. SRP предотвращает связанность из-за нескольких акторов. OCP предотвращает модификацию проверенного кода. LSP предотвращает нарушения поведенческого контракта в иерархиях. ISP предотвращает связанность через толстые интерфейсы. DIP предотвращает зависимость высокоуровневого кода от деталей. Применяйте их там, где присутствует симптом — не как универсальный шаблон для каждого класса.
ℹ️Что дальше: SOLID рассказывает, как назначать ответственности внутри класса и как классы должны зависеть друг от друга. GRASP предлагает дополнительный набор паттернов, специально ориентированных на вопрос: какой объект должен отвечать за что. Следующий урок охватывает все девять паттернов GRASP с примерами.
🔒 Sign in to use this