Принципы SOLID: глубокое погружение
SOLID — это аббревиатура пяти принципов объектно-ориентированного проектирования, введённых Робертом Мартином в начале 2000-х. Каждый принцип адресует конкретный сбой проектирования — симптомы, которые вы узнаёте в реальных кодовых базах: классы, которые невозможно расширить, модули, ломающиеся в неожиданных местах при изменении, код, который нельзя протестировать изолированно. Принципы SOLID — это не правила для механического применения, а линзы для выявления проблем проектирования и направления рефакторинга. В этом уроке каждый принцип разобран точно: сначала нарушение, затем исправление, затем архитектурные последствия.
Формулировка Мартина: «У класса должна быть только одна причина для изменения». Ключевое слово — «причина»: оно относится к актору или стейкхолдеру, чьи требования могут вызвать изменение, а не к синтаксическому правилу «один метод». Класс, который форматирует отчёты И сохраняет их в базу данных, имеет две причины для изменения: команда по отчётности (изменение формата) и команда эксплуатации (миграция базы данных). Когда эти изменения происходят одновременно, они конфликтуют. SRP говорит: эти задачи относятся к разным классам.
// ── НАРУШЕНИЕ: две причины для изменения в одном классе ─────────
class OrderProcessor { void process(Order order) {// бизнес-логика
order.setStatus(CONFIRMED);
// сохранение — причина #2 для изменения
db.save(order);
// уведомление — причина #3 для изменения
emailClient.send(order.getEmail(), buildTemplate(order));
}
}
// ── ИСПРАВЛЕНИЕ: у каждого класса одна причина для изменения ────
class OrderConfirmationService { OrderConfirmationService(OrderRepository repo, OrderNotificationService notifier) { ... } void confirm(Order order) {order.confirm(); // только доменная логика
repo.save(order); // делегирует сохранение
notifier.sendConfirmation(order); // делегирует уведомление
}
}
class OrderRepository { void save(Order o) { ... } } // только БДclass OrderNotificationService { void sendConfirmation(Order o) { ... } } // только emailФормулировка Бертрана Мейера (1988), переосмысленная Мартином: «Программные сущности должны быть открыты для расширения, но закрыты для модификации». Класс закрыт для модификации, когда его проверенный, задеплоенный код не нужно менять для добавления нового поведения. Он открыт для расширения, когда новое поведение добавляется написанием нового кода — нового подкласса, новой реализации стратегии, нового декоратора — без прикосновения к существующему классу. Самый распространённый механизм: зависеть от абстракций (интерфейсов) и внедрять конкретные реализации.
// ── НАРУШЕНИЕ: каждый новый способ оплаты требует модификации ───
class PaymentProcessor { void process(Payment p) {if (p.type() == CREDIT_CARD) chargeCard(p);
else if (p.type() == PAYPAL) chargePayPal(p);
else if (p.type() == CRYPTO) chargeCrypto(p); // добавлено позже — изменён существующий класс
}
}
// ── ИСПРАВЛЕНИЕ: закрыт для модификации, открыт для расширения ──
interface PaymentGateway { void charge(Payment p); }class CreditCardGateway implements PaymentGateway { ... }class PayPalGateway implements PaymentGateway { ... }class CryptoGateway implements PaymentGateway { ... } // новый метод = только новый классclass PaymentProcessor {private final Map<PaymentType, PaymentGateway> gateways;
void process(Payment p) {gateways.get(p.type()).charge(p); // никогда не изменяется
}
}
Формулировка Барбары Лисков (1987): «Если S является подтипом T, то объекты типа T можно заменять объектами типа S, не нарушая желательных свойств программы». Простыми словами: подкласс должен соблюдать контракт суперкласса — не только сигнатуры методов, но и поведенческие ожидания (предусловия, постусловия, инварианты). Классическое нарушение LSP — проблема Квадрата-Прямоугольника: Square расширяет Rectangle, но переопределение setWidth с одновременным изменением height нарушает контракт прямоугольника о независимости ширины и высоты.
// ── НАРУШЕНИЕ: Square нарушает поведенческий контракт Rectangle ─
class Rectangle {protected int width, height;
void setWidth(int w) { this.width = w; } void setHeight(int h) { this.height = h; } int area() { return width * height; }}
class Square extends Rectangle { @Override void setWidth(int w) { width = height = w; } // нарушает контракт Rectangle @Override void setHeight(int h) { width = height = h; } // нарушает контракт Rectangle}
// Клиентский код ломается при подстановке Square вместо Rectangle:
// rect.setWidth(5); rect.setHeight(4); assert rect.area() == 20; // ПРОВАЛ для Square
// ── ИСПРАВЛЕНИЕ: моделируем реальное отношение — без наследования ─
interface Shape { int area(); }record Rectangle(int width, int height) implements Shape { public int area() { return width * height; }}
record Square(int side) implements Shape { public int area() { return side * side; }}
Формулировка Мартина: «Клиенты не должны зависеть от интерфейсов, которые они не используют». Толстые интерфейсы — с множеством несвязанных методов — вынуждают реализаторов создавать заглушки для методов, которые им не нужны, а клиентов — импортировать тип, несущий возможности, которые им не нужны. ISP говорит: предпочитайте множество маленьких клиентских интерфейсов одному большому. Проектное давление ISP естественно порождает Ролевые Интерфейсы — интерфейсы, описывающие роль объекта (Printable, Saveable, Auditable), а не полный перечень всего, что объект умеет.
// ── НАРУШЕНИЕ: толстый интерфейс принуждает к ненужным заглушкам ─
interface Worker {void work();
void takeBreak(); // нерелевантно для Robot
void receivePaycheck(); // нерелевантно для Robot
}
class Robot implements Worker { public void work() { ... } public void takeBreak() { /* Robot не отдыхает — принудительная заглушка */ } public void receivePaycheck() { /* Robot не получает зарплату — заглушка */ }}
// ── ИСПРАВЛЕНИЕ: маленькие ролевые интерфейсы ───────────────────
interface Workable { void work(); }interface Breakable { void takeBreak(); }interface Payable { void receivePaycheck(); }class Robot implements Workable { ... } // только нужноеclass HumanWorker implements Workable, Breakable, Payable { ... } // полный набор// Планировщик нуждается только в Workable — не знает о перерывах и оплате:
class ProductionScheduler { void schedule(List<Workable> workers) { ... } }Формулировка Мартина: «(A) Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. (B) Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций». DIP — это принцип, лежащий в основе Луковой архитектуры, Гексагональной архитектуры и Чистой архитектуры. Он же делает внедрение зависимостей осмысленным: DI — это механизм; DIP — это причина.
// ── НАРУШЕНИЕ: модуль верхнего уровня зависит от детали ─────────
class OrderService {private final MySqlOrderRepository repo = new MySqlOrderRepository();
void confirm(Long id) {Order order = repo.findById(id);
order.confirm();
repo.save(order);
}
}
// ── ИСПРАВЛЕНИЕ: оба зависят от абстракции ───────────────────────
interface OrderRepository { Order findById(Long id); void save(Order o); }class OrderService {private final OrderRepository repo; // зависит от абстракции
OrderService(OrderRepository repo) { this.repo = repo; } // DI через конструктор void confirm(Long id) { ... } // логика не изменилась}
// Низкоуровневая деталь зависит от абстракции (не наоборот):
class MySqlOrderRepository implements OrderRepository { ... } // продакшенclass InMemoryOrderRepository implements OrderRepository { ... } // тесты| Принцип | Устраняемый симптом | Механизм | Архитектурный эффект |
|---|---|---|---|
| SRP | Классы меняются по нескольким несвязанным причинам; конфликты слияния; разрозненные тесты | Разделить по актору/причине изменения | Двигает к модульной декомпозиции; независимый деплой |
| OCP | Добавление фич требует модификации проверенного кода; растущие цепочки if/else | Зависеть от абстракций; расширять добавлением новых реализаций | Плагинные архитектуры, паттерн Strategy, точки расширения |
| LSP | Подклассы, ломающие вызывающий код; UnsupportedOperationException в переопределениях | Соблюдать поведенческий контракт, не только сигнатуры | Гарантирует подстановляемость в полиморфных иерархиях; композиция вместо наследования |
| ISP | Реализация нерелевантных методов; толстые интерфейсы, часто меняющиеся | Маленькие ролевые интерфейсы для каждого клиента | Снижает связанность между модулями |
| DIP | Высокоуровневый код зависит от БД/фреймворка; не тестируется без инфраструктуры | Абстракции в пакете высокого уровня; детали внедряются | Основа Луковой/Гексагональной/Чистой архитектуры; тест-дублёры |