EssayAI
Блог
Блог
Математика и алгоритмы

Принципы SOLID с примерами: разбор пяти правил ООП

19 июня 2026Время чтения: 8 минут
#SOLID#ООП#принципы проектирования#рефакторинг#чистый код
Принципы SOLID с примерами: разбор пяти правил ООП

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

Что скрывается за аббревиатурой SOLID

SOLID - мнемоника из первых букв пяти принципов: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Это не догма и не паттерны проектирования, а ориентиры: они помогают держать связанность (coupling) низкой, а связность (cohesion) внутри модуля - высокой. Когда код соблюдает SOLID, новая функциональность чаще добавляется дописыванием классов, а не правкой десятка существующих файлов.

Важно понимать границы применимости. SOLID родом из ООП-мира с классами и интерфейсами, и в маленьком скрипте на 50 строк строгое следование всем пяти правилам только раздует код лишними абстракциями. Принципы окупаются там, где система живёт и меняется годами, а над ней работает команда.

Схема пяти принципов SOLID: буквы S O L I D с короткими подписями ответственность, расширение, подстановка, интерфейсы, зависимости
Схема пяти принципов SOLID: буквы S O L I D с короткими подписями ответственность, расширение, подстановка, интерфейсы, зависимости

S - Single Responsibility Principle

Принцип единственной ответственности гласит: у класса должна быть только одна причина для изменения. Формулировка Мартина точнее звучит так - класс должен отвечать перед одним «актором» (одной группой заинтересованных лиц). Если отчёт о зарплате правят и бухгалтерия, и отдел кадров, и DBA, - у класса три причины меняться, и это нарушение SRP.

Классический антипример - «божественный» класс, который и считает, и сохраняет, и шлёт уведомления:

Employee    {расчёт, сохранение в БД, отправка письма}\text{Employee} \;\Rightarrow\; \{\,\text{расчёт},\ \text{сохранение в БД},\ \text{отправка письма}\,\}
class Employee:
    def calculate_pay(self): ...      # бизнес-логика
    def save_to_db(self): ...         # persistence
    def send_email(self): ...         # уведомления

Решение - разнести обязанности по разным классам: PayCalculator, EmployeeRepository, EmailNotifier. Тогда смена СУБД не заденет формулу зарплаты, а правка текста письма - логику расчёта. Тесты тоже упрощаются: каждый класс проверяется изолированно.

O - Open-Closed Principle

Принцип открытости/закрытости: программные сущности должны быть открыты для расширения, но закрыты для изменения. То есть новое поведение добавляется без правки уже работающего и оттестированного кода. Сигнал нарушения OCP - длинная цепочка if/switch по типу, которую приходится дополнять при каждой новой разновидности.

Было (каждая новая фигура - правка метода area):

double area(Shape s) {
    if (s instanceof Circle c)   return Math.PI * c.r * c.r;
    if (s instanceof Square sq)  return sq.side * sq.side;
    // добавили треугольник - лезем сюда снова
}

Стало - каждая фигура сама знает свою площадь через общий интерфейс:

interface Shape { double area(); }
class Circle implements Shape { public double area() { return Math.PI * r * r; } }
class Square implements Shape { public double area() { return side * side; } }

Новый Triangle implements Shape подключается, не трогая ни строчки старого кода. Полиморфизм и абстракции - главный инструмент OCP. Похожую логику «расширяемого поведения» полезно держать в голове, когда разбираешь полиморфизм в ООП на примере.

L - Liskov Substitution Principle

Принцип подстановки Барбары Лисков: объект базового класса можно заменить объектом любого наследника без нарушения корректности программы. Если код, написанный против Bird, ломается, когда вместо обычной птицы подставили Penguin, - наследование выстроено неверно.

Хрестоматийный пример - квадрат и прямоугольник. Математически квадрат «является» прямоугольником, но если Square extends Rectangle и переопределяет сеттеры так, что setWidth(5) молча меняет и высоту, любой клиент, ожидающий независимые стороны, получит сюрприз:

после setWidth(5); setHeight(4)area=16  20\text{после } \texttt{setWidth(5);\ setHeight(4)} \quad\Rightarrow\quad \text{area} = 16 \ \ne\ 20

LSP требует, чтобы наследник не усиливал предусловия и не ослаблял постусловия базового типа. Нарушения LSP часто маскируются проверками instanceof и выбрасыванием UnsupportedOperationException в переопределённом методе - это явный признак, что иерархия спроектирована против принципа.

Принцип подстановки Лисков: контракт базового типа и наследник, который должен подходить вместо родителя
Принцип подстановки Лисков: контракт базового типа и наследник, который должен подходить вместо родителя

I - Interface Segregation Principle

Принцип разделения интерфейсов: клиент не должен зависеть от методов, которыми он не пользуется. Большой «толстый» интерфейс заставляет реализующие классы писать пустые или бросающие исключение методы. Лучше несколько узких интерфейсов, чем один универсальный.

Антипример - интерфейс Worker { work(); eat(); sleep(); }, который реализует и человек, и робот. Робот не ест и не спит, но вынужден реализовать eat() заглушкой:

interface Workable { work(): void; }
interface Eatable { eat(): void; }

class Robot implements Workable { work() {} }          // только то, что нужно
class Human implements Workable, Eatable { work() {} eat() {} }

Теперь робот зависит только от Workable. ISP тесно связан с SRP: если интерфейс делает слишком много, его, скорее всего, стоит разбить. Узкие интерфейсы делают зависимости явными и упрощают подмену в тестах через моки.

D - Dependency Inversion Principle

Принцип инверсии зависимостей: модули верхнего уровня не должны зависеть от модулей нижнего уровня - оба должны зависеть от абстракций. И абстракции не должны зависеть от деталей, наоборот - детали зависят от абстракций. На практике это значит: бизнес-логика работает с интерфейсом, а конкретную реализацию (БД, HTTP-клиент, очередь) ей подсовывают извне.

Было - сервис жёстко привязан к конкретной СУБД:

class OrderService {
    private MySqlRepository repo = new MySqlRepository(); // деталь зашита внутрь
}

Стало - сервис зависит от абстракции, реализацию передают через конструктор (внедрение зависимости):

class OrderService {
    private readonly IOrderRepository repo;
    public OrderService(IOrderRepository repo) { this.repo = repo; }
}

Теперь MySqlRepository, PostgresRepository или InMemoryRepository для тестов подставляются снаружи. DIP - фундамент для Dependency Injection и инверсии управления (IoC-контейнеры). Не путайте сам принцип (зависеть от абстракций) с приёмом DI (передавать зависимость снаружи): DI - один из способов реализовать DIP.

Как принципы работают вместе

SOLID - не пять изолированных правил, а связанная система. SRP подсказывает, где провести границы классов; ISP применяет ту же идею к интерфейсам; OCP и DIP вместе дают расширяемость через абстракции; LSP следит, чтобы эти абстракции были честными и наследники их не предавали. На практике рефакторинг по SOLID обычно идёт волной: вынесли ответственность (SRP) → выделили интерфейс (ISP) → начали зависеть от него (DIP) → новое поведение добавляется без правок (OCP), а подстановка реализаций безопасна (LSP).

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

  • Карго-культ SOLID. Плодить интерфейс на каждый класс «на всякий случай» в небольшом проекте - это лишняя сложность, а не чистый код. Принципы решают конкретные проблемы изменяемости, а не существуют ради себя.
  • Путать SRP с «один метод на класс». Единственная ответственность - это одна причина изменения, а не одна функция. Класс может иметь несколько методов, пока все они служат одной ответственности.
  • Считать DIP и DI синонимами. DIP - принцип (зависеть от абстракций), DI - техника передачи зависимости. Можно нарушать DIP даже с DI-контейнером, если внедрять конкретные классы.
  • Нарушать LSP заглушками. throw new UnsupportedOperationException() в переопределённом методе - почти всегда сигнал неправильной иерархии, а не «защита от ошибки».
  • Толстые интерфейсы из удобства. Один интерфейс «на всё» кажется проще, но заставляет классы реализовывать ненужные методы и связывает клиентов с тем, что им не нужно.

FAQ

В каком порядке учить принципы SOLID? Удобно начать с SRP и OCP - они самые наглядные и встречаются чаще всего. Затем DIP, потому что он лежит в основе тестируемого кода и внедрения зависимостей. LSP и ISP проще понять после того, как освоено наследование и интерфейсы на практике.

Применим ли SOLID вне ООП? Идеи переносятся и в функциональное, и в процедурное программирование: единственная ответственность функции, расширение через функции высшего порядка, зависимость от абстрактных контрактов. Но формулировки SOLID завязаны на классы и интерфейсы, поэтому вне ООП их адаптируют, а не применяют дословно.

Чем SOLID отличается от паттернов проектирования? SOLID - это принципы, то есть критерии «хорошо/плохо» для структуры кода. Паттерны (Стратегия, Фабрика, Адаптер) - готовые рецепты решения типовых задач. Часто паттерны как раз и помогают соблюсти SOLID: например, Стратегия реализует OCP, а Фабрика помогает с DIP.

Коротко

Принципы SOLID - пять ориентиров ООП-проектирования: SRP (одна причина изменения), OCP (расширять, не меняя), LSP (наследник подходит вместо родителя), ISP (узкие интерфейсы), DIP (зависеть от абстракций). Их цель - снизить связанность и упростить развитие кода, а не усложнить его абстракциями ради абстракций. Разбирайте каждый принцип на конкретном нарушении и его рефакторинге - так разница «до и после» становится очевидной.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

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