SOLID

Общее

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

Термин SOLID ввёл Роберт Мартин (Robert C. Martin, известный как Uncle Bob) в начале 2000-х годов. Сам акроним впервые появился в статье, написанной Робертом Мартином по просьбе Майкла Фэзерса (Michael Feathers) примерно в 2000 году. Сами принципы, однако, опираются на более ранние работы: принцип подстановки сформулировала Барбара Лисков (Barbara Liskov) в 1987 году, а принципы единственной ответственности, открытости/закрытости и разделения интерфейсов восходят к трудам Бертрана Мейера (Bertrand Meyer) и других исследователей объектно-ориентированного проектирования. Мартин собрал и систематизировал их, показав взаимосвязь, и описал в книге Agile Software Development: Principles, Patterns, and Practices (2002), а позднее — в Clean Architecture (2017).

SOLID исторически связан с объектно-ориентированным программированием (ООП), где принципы применяются к классам и интерфейсам. Однако ценность SOLID выходит за рамки отдельного класса: те же идеи — разделение ответственности, защита от изменений, подставляемость компонентов, узкие контракты и инверсия зависимостей — масштабируются на уровень модулей, компонентов и архитектурных слоёв. Именно поэтому SOLID считается отправной точкой для понимания модульности, паттернов проектирования и Clean Architecture.

Назначение SOLID — не набор догм, а фильтр против типичных ошибок проектирования: разрастания классов до «божественных объектов», хрупкости иерархий наследования, жёстких зависимостей между модулями и раздутых интерфейсов, которые заставляют клиентов зависеть от того, чем они не пользуются. Каждый принцип можно сформулировать коротко: он отвечает на конкретный проектный вопрос и указывает на характерный «запах» (code smell), возникающий при его нарушении.

Краткая сводка принципов:

Буква Принцип Суть
S Single Responsibility У класса одна причина для изменения
O Open/Closed Открыт для расширения, закрыт для изменения
L Liskov Substitution Подтип можно подставить вместо базового типа
I Interface Segregation Клиенты не зависят от неиспользуемых методов
D Dependency Inversion Зависимость направлена на абстракции, а не на реализации

Принципы SOLID

Single Responsibility Principle

Оригинальная формулировка. A class should have one, and only one, reason to change. — У класса должна быть одна и только одна причина для изменения.

Мотивация. «Причина для изменения» в формулировке Мартина — это, по сути, группа заинтересованных лиц или аспект системы, инициирующий изменения. Если класс отвечает за несколько разнородных задач (например, за бизнес-логику, сохранение в базу данных и формирование отчёта), его меняют по независимым поводам: изменение формата отчёта не должно затрагивать логику расчётов. Смешение ответственностей приводит к тому, что любое касательное изменение рискует сломать не связанные с ним функции, а класс быстро превращается в «божественный объект» (God Object).

Антипаттерн-нарушение. Класс, совмещающий бизнес-правила и работу с хранилищем:

class Report {
  public string Title { get; set; }
  public string Body { get; set; }

  public void Generate() {
    // бизнес-логика формирования отчёта
    Body = "...";
  }

  public void SaveToDatabase() {
    // работа с хранилищем: SQL, соединения, транзакции
  }

  public void ExportToPdf() {
    // форматирование и рендер PDF
  }
}

Класс Report меняется по трём независимым поводам: изменится структура отчёта — правится Generate; поменяется схема базы данных — правится SaveToDatabase; обновится библиотека генерации PDF — правится ExportToPdf. Каждое изменение затрагивает один и тот же файл, что повышает риск регрессий.

Исправление. Разнести ответственности по отдельным классам, каждый из которых имеет одну причину для изменения:

class Report {
  public string Title { get; set; }
  public string Body { get; set; }
  public void Generate() { /* бизнес-логика */ }
}

class ReportRepository {
  public void Save(Report report) { /* работа с хранилищем */ }
}

class PdfExporter {
  public byte[] Export(Report report) { /* рендер PDF */ }
}

Теперь изменение формата PDF не затрагивает ни бизнес-логику, ни хранилище. Принцип не требует разнесения ради самого разнесения — он требует, чтобы у каждого класса была одна согласованная ответственность.

Open/Closed Principle

Оригинальная формулировка. Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. — Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Мотивация. Принцип введён Бертраном Мейером в 1988 году. Его цель — сделать систему устойчивой к добавлению новой функциональности. «Открыт для расширения» означает, что поведение сущности можно дополнить; «закрыт для модификации» — что для этого не нужно править исходный код уже работающей и протестированной сущности. Если добавление каждого нового варианта требует менять существующие классы (добавлять switch/if-else ветки), система становится хрупкой: любое изменение рискует сломать уже работающие сценарии.

Антипаттерн-нарушение. Класс калькулятора скидок, который нужно править при каждом новом типе клиента:

class DiscountCalculator {
  public double calculate(Customer customer) {
    if (customer.getType().equals("regular")) {
      return 0.05;
    } else if (customer.getType().equals("vip")) {
      return 0.20;
    } else if (customer.getType().equals("employee")) {
      return 0.30;
    }
    return 0.0;
  }
}

Добавление нового типа клиента («wholesale») требует открыть класс DiscountCalculator и добавить новую ветку — то есть модифицировать уже работающий и протестированный код.

Исправление. Вынести вариативное поведение в абстракцию (полиморфизм или стратегию), чтобы новые варианты добавлялись новыми классами:

interface DiscountPolicy {
  double calculate(Customer customer);
}

class RegularDiscount implements DiscountPolicy {
  public double calculate(Customer c) { return 0.05; }
}

class VipDiscount implements DiscountPolicy {
  public double calculate(Customer c) { return 0.20; }
}

class EmployeeDiscount implements DiscountPolicy {
  public double calculate(Customer c) { return 0.30; }
}

Добавление нового типа сводится к созданию класса WholesaleDiscount — существующий код не трогается. Принцип достигается через абстракции (интерфейсы, абстрактные классы) и полиморфизм, а также через паттерны Strategy, Template Method, Decorator.

Liskov Substitution Principle

Оригинальная формулировка. Сформулирована Барбарой Лисков в 1987 году: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. — Если для каждого объекта o1 типа S существует объект o2 типа T, такой что все программы P, определенные в терминах T, не меняют своего поведения при замене o2 на o1, то S является подтипом T.

Роберт Мартин дал более прагматичную переформулировку: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. — Функции, использующие ссылки на базовый класс, должны иметь возможность использовать объекты производных классов, ничего не зная о них.

Мотивация. Принцип подстановки гарантирует, что иерархия наследования действительно выражает отношение «является» (is-a), а не просто повторное использование кода. Если подтип нарушает контракт базового типа — усиливает предусловия, ослабляет постусловия, выбрасывает неожиданные исключения или не выполняет обещанные действия, — то клиенты, рассчитывающие на базовый тип, сломаются при подстановке подтипа. Это делает полиморфизм ненадёжным и вынуждает клиентов проверять конкретный тип (instanceof), что разрушает абстракцию.

Антипаттерн-нарушение. Классический пример — «квадрат и прямоугольник»:

class Rectangle {
  protected int width;
  protected int height;

  public void setWidth(int w) { width = w; }
  public void setHeight(int h) { height = h; }
  public int area() { return width * height; }
}

class Square extends Rectangle {
  public void setWidth(int w)  { width = w; height = w; }
  public void setHeight(int h) { width = h; height = h; }
}

Клиент, работающий через базовый тип, логично ожидает независимого изменения сторон:

void resize(Rectangle r) {
  r.setWidth(5);
  r.setHeight(10);
  assert r.area() == 50;  // ломается для Square: площадь = 100
}

Square нарушает контракт Rectangle: независимая установка ширины и высоты перестаёт работать. Подстановка подтипа меняет поведение программы — принцип Лисков нарушен.

Исправление. Не использовать наследование там, где не выполняется подстановка. Квадрат не является прямоугольником с точки зрения изменяемого интерфейса — у него одна сторона. Решения:

  • Не делать Square наследником Rectangle; ввести общий предок (например, абстракцию формы) или сделать их независимыми классами.
  • Использовать неизменяемые объекты (immutable): неизменяемый квадрат и неизменяемый прямоугольник, созданные через фабрики, не нарушают контракт, так как не имеют сеттеров.

Принцип Лисков также формализуется через контракты (Design by Contract): подтип не должен усиливать предусловия и не должен ослаблять постусловия; не должен выбрасывать новые типы исключений, кроме подтипов исключений базового типа.

Interface Segregation Principle

Оригинальная формулировка. Clients should not be forced to depend upon interfaces that they do not use. — Клиенты не должны вынужденно зависеть от методов, которые они не используют. Принцип сформулирован Робертом Мартином при работе над Xerox и связан с именами Рекса Шмидта (Rex Schmidt) и др.

Мотивация. «Толстый» интерфейс, объединяющий много методов, вынуждает каждого клиента зависеть от всего интерфейса целиком — даже от тех методов, которые он не вызывает. Это приводит к непреднамеренным связям: изменение метода, не используемого клиентом, всё равно затрагивает его (перекомпиляция, риск поломки при изменении сигнатуры, фиктивные реализации методов-заглушек). Кроме того, классы вынуждены реализовывать методы, которые для них бессмысленны, что часто выражается в выбросе NotImplementedException или UnsupportedOperationException.

Антипаттерн-нарушение. Единый интерфейс для многофункционального устройства:

interface MultiFunctionDevice {
  void print(Document d);
  void scan(Document d);
  void fax(Document d);
}

class SimplePrinter implements MultiFunctionDevice {
  public void print(Document d) { /* печать */ }
  public void scan(Document d)  { throw new UnsupportedOperationException(); }
  public void fax(Document d)   { throw new UnsupportedOperationException(); }
}

SimplePrinter вынужден зависеть от scan и fax и фиктивно их реализовывать. Добавление нового метода в интерфейс затронет даже те клиенты, которым он не нужен.

Исправление. Разделить «толстый» интерфейс на узкие, сфокусированные интерфейсы (role interfaces):

interface Printer { void print(Document d); }
interface Scanner { void scan(Document d); }
interface Fax     { void fax(Document d); }

class SimplePrinter implements Printer {
  public void print(Document d) { /* печать */ }
}

class MultiFunctionPrinter implements Printer, Scanner, Fax {
  public void print(Document d) { /* ... */ }
  public void scan(Document d)  { /* ... */ }
  public void fax(Document d)   { /* ... */ }
}

Теперь простой принтер зависит только от того, что использует. Принцип ISP тесно связан с SRP: узкие интерфейсы соответствуют узким ответственностям и снижают связанность между клиентами.

Dependency Inversion Principle

Оригинальная формулировка. Состоит из двух частей:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions. — Высокоуровневые модули не должны зависеть от низкоуровневых. Оба должны зависеть от абстракций.
  2. Abstractions should not depend on details. Details should depend on abstractions. — Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Мотивация. Без инверсии зависимостей высокоуровневая политика (бизнес-логика) напрямую использует низкоуровневые детали (конкретные классы доступа к данным, внешние сервисы, библиотеки). Это означает, что бизнес-правила жёстко связаны с инфраструктурой: замена базы данных, почтового сервиса или HTTP-клиента требует изменения бизнес-логики. Более того, такая зависимость препятствует тестированию — бизнес-логику невозможно проверить без живой базы данных или сети.

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

Антипаттерн-нарушение. Сервис бизнес-логики напрямую создаёт и использует конкретный репозиторий:

class OrderService {
  private SqlOrderRepository repository = new SqlOrderRepository();

  public void placeOrder(Order order) {
    // бизнес-логика зависит от конкретного SqlOrderRepository
    repository.save(order);
  }
}

OrderService (высокоуровневый модуль) жёстко зависит от SqlOrderRepository (низкоуровневой детали). Замена на MongoOrderRepository или использование заглушки в тестах требует правки OrderService.

Исправление. Ввести абстракцию, которой принадлежат оба модуля, и внедрить зависимость (dependency injection):

interface OrderRepository {
  void save(Order order);
}

class OrderService {
  private final OrderRepository repository;

  public OrderService(OrderRepository repository) {
    this.repository = repository;  // внедрение через конструктор
  }

  public void placeOrder(Order order) {
    repository.save(order);  // зависимость от абстракции
  }
}

class SqlOrderRepository implements OrderRepository {
  public void save(Order order) { /* реализация для SQL */ }
}

Теперь OrderService зависит от OrderRepository (абстракции), а SqlOrderRepository — реализует её. Направление зависимости «перевёрнуто»: низкоуровневый модуль зависит от абстракции высокоуровневого. Подставляя mock-реализацию в тестах, бизнес-логику можно проверять изолированно. Принцип DIP лежит в основе паттернов Dependency Injection, Inversion of Control и механизмов фреймворков (Spring, .NET DI и др.).

SOLID на уровне архитектуры

Ценность SOLID не исчерпывается уровнем отдельных классов. Роберт Мартин в Clean Architecture показывает, что те же пять принципов масштабируются на уровень компонентов и слоёв архитектуры, образуя фундамент модульности и защищённости границ.

От классов к компонентам. На уровне компонентов (модулей, библиотек, микросервисов) действуют те же идеи, переформулированные как принципы композиции компонентов (REP, CCP, CRP, ADP, SDP, SAP). Суть остаётся прежней: компонент должен иметь согласованные ответственности (аналог SRP), быть открытым для расширения (аналог OCP), предоставлять узкие, сфокусированные интерфейсы (аналог ISP), и зависимости между компонентами должны быть направлены в сторону стабильности — то есть в сторону абстракций (аналог DIP).

Границы компонентов (component boundaries). Архитектурная граница — это линия, разделяющая компоненты, которая проводится там, где направление зависимости важно. Хорошо проведённая граница гарантирует, что низкоуровневые детали (база данных, веб-фреймворк, внешние API) зависят от высокоуровневой политики, а не наоборот.

Dependency Inversion как фундамент границ. Именно DIP позволяет провести архитектурную границу в системе, где исходно зависимость направлена «не туда». Если бизнес-логика должна вызывать инфраструктуру, но не должна зависеть от неё, вводится интерфейс на стороне бизнес-логики; инфраструктура реализует этот интерфейс, и направление зависимости переворачивается. На этом механизме строятся архитектурные шаблоны:

  • Ports and Adapters (Hexagonal Architecture, Алистер Кокбёрн): ядро приложения определяет «порты» (интерфейсы), а адаптеры (БД, веб, UI) их реализуют.
  • Clean Architecture (Роберт Мартин): концентрические слои — Entities, Use Cases, Interface Adapters, Frameworks & Drivers; правило зависимостей (Dependency Rule) направляет стрелки внутрь, к абстракциям.
  • Onion Architecture (Джеффри Палермо): аналогичная идея слоёв, ориентированных на доменное ядро.

Связь с Clean Architecture. В Clean Architecture каждый слой зависит только от внутренних, более стабильных слоёв. Принцип подстановки Лисков обеспечивает корректность полиморфных замен между слоями; принцип открытости/закрытости позволяет добавлять новые варианты (новые контроллеры, новые адаптеры БД) без правок ядра; принцип разделения интерфейсов не даёт ядру зависеть от избыточных деталей адаптеров. Таким образом, SOLID на уровне классов и SOLID на уровне архитектуры — это один и тот же набор идей, применённый в разных масштабах.

Критика и границы применимости

SOLID — не универсальное решение, и его механическое применение может принести вред. Критика принципов и границы их применимости сводятся к нескольким аспектам.

Переусложнение. Слепое применение SOLID к каждой задаче ведёт к избыточной абстракции: появляется множество мелких классов, интерфейсов с единственной реализацией и уровней косвенности, которые не приносят пользы, но усложняют понимание кода. Мартин Фаулер и другие авторы предостерегают от «догматического SOLID» — принципы должны служить средству, а не самоцели. Часто простой процедурный код понятнее, чем раздутое многоуровневое ООП.

Не везде нужен ООП. SOLID сформулирован в парадигме объектно-ориентированного проектирования. В функциональном программировании (ФП) многие проблемы, которые SOLID решает через классы и интерфейсы, вообще не возникают: неизменяемые данные и чистые функции естественным образом изолируют ответственность, а композиция функций заменяет наследование. В ФП вместо подстановки типов используется композиция, вместо инверсии зависимостей — передача функций как аргументов (higher-order functions). SOLID остаётся осмысленным концептуально, но его буквальная реализация через интерфейсы и классы неприменима.

Динамические языки. В динамически типизированных языках (Python, Ruby, JavaScript) формальные интерфейсы и абстрактные классы часто отсутствуют. Часть принципов (ISP, DIP) реализуется через утиную типизацию (duck typing), протоколы (PEP 544 в Python) или структурную типизацию (TypeScript). LSP проверяется на уровне поведения и тестов, а не на уровне системы типов. Это не отменяет SOLID, но меняет инструментарий.

YAGNI vs SOLID. Принцип YAGNI («You Aren’t Gonna Need It» — из XP Кента Бека) предостерегает от добавления функциональности «на вырост». SOLID, особенно OCP и DIP, при буквальном прочтении подталкивает к раннему введению абстракций «чтобы было легко расширять». Конфликт разрешается прагматично: абстракции вводятся, когда второе аналогичное изменение подтверждает потребность (правило трёх), а не заранее. Переход от конкретного к абстрактному выполняется через рефакторинг, когда накапливается достаточно информации о направлениях изменения.

Эмпирическая неоднозначность. Ряд исследований и прагматичных практиков (например, Дэн Норт, автор BDD, в докладе «SOLID is not solid») указывают, что буквальное соблюдение SOLID коррелирует с ростом числа классов и косвенности, что не всегда улучшает сопровождаемость. Принципы полезны как диагностические вопросы («есть ли у класса несколько причин для изменения?»), но не как обязательные к исполнению правила.

Плюсы

Устойчивость к изменениям. Хорошо спроектированные по SOLID системы локализуют изменения: новая функциональность добавляется новыми классами, а не правками в существующих. Это снижает риск регрессий.

Слабая связанность и высокая связность. Разделение ответственностей и узкие интерфейсы уменьшают зависимости между модулями. Компоненты можно изменять и тестировать независимо.

Тестируемость. Инверсия зависимостей позволяет подставлять mock-объекты и заглушки, делая модульное тестирование изолированным и быстрым. Бизнес-логику можно проверять без базы данных, сети и внешних сервисов.

Переиспользуемость. Классы и модули с единственной ответственностью и узкими интерфейсами легче переносятся между проектами и контекстами.

Понятность и коммуникация. Единый словарь принципов облегчает обсуждение проектных решений внутри команды: формулировки SRP или DIP служат общим языком для аргументации.

Масштабируемость на архитектуру. Те же принципы работают на уровне компонентов и слоёв, образуя фундамент Clean Architecture и паттернов проектирования — единый набор идей от класса до системы.

Защита от типичных ошибок. Каждый принцип нацелен на конкретный антипаттерн (God Object, жёсткие зависимости, хрупкие иерархии, раздутые интерфейсы), давая эвристику для их распознавания и устранения.

Минусы

Риск переусложнения. Без меры SOLID порождает избыточную абстракцию: интерфейсы с единственной реализацией, лишние уровни косвенности и мелкие классы, затрудняющие понимание потока выполнения.

Зависимость от парадигмы. Принципы сформулированы для ООП; в функциональном и процедурном коде их буквальное применение через классы и интерфейсы неприменимо или избыточно.

Кривая обучения. Корректное применение SOLID требует опыта: начинающие разработчики часто применяют принципы механически, ухудшая, а не улучшая код.

Затраты на начальное проектирование. Введение абстракций и разделение ответственностей требует времени и усилий, что может быть избыточно для прототипов и одноразовых скриптов.

Конфликт с YAGNI. Раннее абстрагирование «на вырост» противоречит принципу YAGNI; баланс между гибкостью и простотой требует инженерного суждения.

Невозможность механической проверки. В отличие от форматирования, SOLID нельзя полностью проверить автоматически: «причина для изменения» или «нарушение контракта» — понятия семантические, требующие контекста предметной области.

Связанные статьи

  • Введение в архитектуру — общие понятия архитектуры ПО, в том числе место SOLID среди базовых принципов.
  • Паттерны проектирования — порождающие, структурные и поведенческие шаблоны, реализующие принципы SOLID на практике (Strategy и OCP, Adapter и ISP, Decorator и OCP).
  • ADR — Architecture Decision Records — практика фиксации архитектурных решений, включая выбор и обоснование применения SOLID.
  • Clean Architecture — планируемая статья о послойной архитектуре с правилом зависимостей, где SOLID (в первую очередь DIP) формирует архитектурные границы.