📐 Раздел 18 · Вопрос #20

Можно ли следовать всем принципам SOLID одновременно

SOLID — это не математические законы и не юридический кодекс. Это набор инженерных эвристик и рекомендаций, призванных снижать стоимость поддержки сложного кода.


🟢 Junior Level

Краткий ответ: Теоретически — да, но на практике следовать всем пяти принципам абсолютно везде не только не нужно, но и вредно.

SOLID — это не математические законы и не юридический кодекс. Это набор инженерных эвристик и рекомендаций, призванных снижать стоимость поддержки сложного кода.

Попытка применить все 5 принципов к каждому классу в проекте превращает простую программу в нечитаемый лабиринт из сотен интерфейсов, фабрик и адаптеров.

Житейская аналогия

Строительство собачьей будки:

  • Если вы строите 50-этажный небоскреб, вам необходимы геологические изыскания, сейсмоустойчивый фундамент, независимые пожарные отсеки и дублирующие лифтовые шахты (полный SOLID).
  • Если вы строите деревянную будку для собаки во дворе, использование тех же норм приведет к тому, что вы потратите $100 000 и 6 месяцев на проект будки с автономной системой вентиляции и лифтом. Будка должна быть просто надежной и теплой.

Пример вредного фанатизма (Over-engineering)

Абсурдное следование SOLID для тривиальной операции:

// 5 файлов ради сложения двух чисел:
public interface AdditionOperation {
    int execute(int a, int b);
}

public class IntegerAdditionStrategy implements AdditionOperation {
    @Override
    public int execute(int a, int b) { return a + b; }
}

public class CalculatorService {
    private final AdditionOperation addition;
    public CalculatorService(AdditionOperation addition) { this.addition = addition; }
    public int add(int a, int b) { return addition.execute(a, b); }
}

Здравый инженерный подход (KISS):

public class Calculator {
    public static int add(int a, int b) {
        return a + b;
    }
}

🟡 Middle Level

Внутренние противоречия и конфликты с другими принципами

Стремление соблюсти все принципы SOLID одновременно сталкивается с фундаментальными архитектурными противоречиями:

1. SOLID против KISS (Keep It Simple, Stupid) и YAGNI

  • Конфликт: Принцип OCP требует создавать точки расширения (интерфейсы, фабрики, реестры стратегий). Но принцип YAGNI (You Aren’t Gonna Need It) предостерегает: «Не пиши код для сценариев, которые тебе сейчас не нужны».
  • В 80% случаев гипотетическое «расширение в будущем» никогда не происходит, а проект остается обременен лишними слоями косвенности.

2. SRP и ISP против Связности (Coupling)

  • Принцип SRP требует делить классы на минимальные ответственности, а ISP — дробить интерфейсы на минимальные роли.
  • Доведение этого до фанатизма приводит к антипаттерну «Атомарная пыль» (Atomic Dust): сотни микроклассов по 5 строк. Чтобы понять сценарий оформления заказа, разработчик вынужден держать открытыми 25 вкладок в IDE. Внутренняя связность модулей (Coupling) становится катастрофической.

3. SOLID против Производительности (Low-Latency / Highload)

  • Каждое разделение по SRP требует создания дополнительного объекта в куче (Heap), увеличивая нагрузку на Garbage Collector.
  • Каждый вызов через интерфейс по DIP/OCP — это invokeinterface и потенциальный мегаморфный диспатчинг, блокирующий JIT-инлайнинг компилятора C2.

Градиентная стратегия применения SOLID

Опытная команда применяет принципы SOLID не бинарно («всё или ничего»), а градиентно, в зависимости от зрелости и назначения модуля:

Слой / Этап системы Уровень строгости SOLID Приоритетные принципы Обоснование
Прототипы, Spikes, MVP Минимальный KISS, DRY Главная цель — проверить гипотезу за 3 дня, а не спроектировать вечный фундамент.
Инфраструктурные утилиты Низкий Статические методы (StringUtils) Стабильные алгоритмы без состояния, которые никогда не будут полиморфно подменяться.
Hot-Path (Highload ядро) Ограниченный Процедурная скорость Устранение виртуальных вызовов и аллокаций памяти ради Latency в микросекундах.
Core Domain (Бизнес-логика) Высокий Все принципы SOLID Самая дорогая часть системы, живущая годами и постоянно меняющаяся под давлением бизнеса.

🔴 Senior Level

Аппаратная цена абстракций на уровне JVM

Senior-инженер четко осознает физическую цену «чистой архитектуры» под нагрузкой:

Бенчмарк для обработки 10 000 000 транзакций (Hotspot C2):
┌──────────────────────────────┬──────────────────┬─────────────────┬────────────────┐
│ Вариант реализации           │ Latency (p99)    │ Аллокация Heap  │ GC Pauses      │
├──────────────────────────────┼──────────────────┼─────────────────┼────────────────┤
│ Прямой процедурный вызов     │ 1.2 ns           │ 0 байт          │ 0 ms           │
│ SOLID (Интерфейс + Стратегия)│ 8.5 ns (+600%)   │ 24 байта / вызов│ 120 ms (Young) │
└──────────────────────────────┴──────────────────┴─────────────────┴────────────────┘
  1. VTable / ITable Overhead: Если интерфейс OCP имеет более двух реализаций в рантайме, точка вызова становится мегаморфной. JIT-компилятор отключает Monomorphic Inline Caching, выполняя косвенный переход по указателю в таблице интерфейсов со сбросом предсказателя переходов процессора (Branch Misprediction).
  2. Object Header Padding: Разделение монолитного объекта на 5 мелких классов по SRP создает 5 заголовков объектов (Object Headers) по 12–16 байт каждый + выравнивание до 8 байт (Padding). При 100 000 RPS это означает гигабайты мусора в секунду в поколении Eden.

Архитектурный компромисс: Expression Problem и Sealed Hierarchies

В Java 21 появление Sealed Interfaces и Pattern Matching for switch окончательно разрушило миф о том, что нужно всегда следовать классическому OCP:

  • Полиморфизм OCP (Интерфейсы + Реализации): открыт для добавления новых типов данных, но закрыт и неудобен для добавления новых операций (нужно менять методы во всех 20 классах).
  • Sealed Types (Функциональная декомпозиция): закрыт для добавления новых типов данных (список permits строго фиксирован), но идеально открыт для добавления новых операций через внешний switch.

Вывод для Senior: Попытка следовать OCP в закрытых предметных областях (например, статусная модель заказа: NEW, PAID, CANCELLED) — это ошибка проектирования. Здесь sealed interface со switch превосходит классический SOLID по типобезопасности и выразительности.


4 Tricky Questions

1. Как конфликт между OCP и YAGNI решается на практике с помощью «Правила трех» (Rule of Three)?

Ответ: «Правило трех» гласит:

  1. Когда вы пишете код в первый раз — сделайте его максимально простым (KISS, без абстракций).
  2. Когда аналогичная логика понадобилась во второй раз — допустите дублирование, но обратите внимание на сходство.
  3. Только когда та же самая вариативность возникает в третий раз — проводите рефакторинг к OCP (выделяйте интерфейс, стратегию или фабрику). Это предохраняет проект от спекулятивной генерализации (Speculative Generality) и создания десятков пустых интерфейсов с единственной реализацией.

2. Является ли создание интерфейса с суффиксом Impl (UserService -> UserServiceImpl) соблюдением SOLID?

Ответ: В 90% случаев это антипаттерн механического карго-культа (Header Interface), а не соблюдение SOLID. Если у интерфейса существует ровно одна реализация и класс не является публичным API сторонней библиотеки, интерфейс не дает никакой полиморфной гибкости (OCP) и не инвертирует зависимости (DIP), а лишь удваивает число файлов в проекте. Интерфейс должен создаваться только тогда, когда есть как минимум две разные реализации (например, PostgresUserRepository и InMemoryUserRepository), либо когда необходимо скрыть внутреннюю реализацию за границей архитектурного модуля.

3. Можно ли нарушать SOLID в JPA Entity-классах и почему?

Ответ: Да, в JPA/Hibernate сущностях принципы SOLID нарушаются осознанно:

  • Сущность базы данных объединяет в себе схему таблицы (поля), бизнес-валидацию, методы жизненного цикла (@PrePersist) и аннотации маппинга (нарушение SRP).
  • Сущности содержат публичные геттеры и сеттеры для рефлексии Hibernate (нарушение инкапсуляции). Попытка декомпозировать JPA Entity на мелкие классы по строгому SRP разрушает ORM-маппинг и приводит к катастрофическому падению производительности SQL-запросов (проблема N+1, избыточные JOIN). Инженеры изолируют это нарушение внутри слоя доступа к данным (Data Access Layer), не выпуская сущности JPA в доменную бизнес-логику.

4. Что такое «Архитектура, управляемая болью» (Pain-Driven Development) в контексте применения SOLID?

Ответ: Это прагматичная инженерная философия, согласно которой рефакторинг кода в сторону SOLID проводится не превентивно, а только тогда, когда текущая структура начинает причинять реальную боль команде:

  • Стало больно писать юнит-тесты $\implies$ внедряем DIP (интерфейсы и DI).
  • Каждое изменение ломает несвязанные модули $\implies$ распиливаем класс по SRP.
  • Разные ветки if-else постоянно конфликтуют в Git $\implies$ выносим логику в стратегии по OCP. Если код работает, покрыт тестами и не меняется годами — следовать SOLID для него не имеет никакого экономического смысла.

🎯 Шпаргалка для интервью

Главные тезисы (30 секунд на ответ):

  • Главный тезис: Следовать всем принципам SOLID абсолютно везде невозможно и контрпродуктивно (риск Over-engineering и нарушения KISS/YAGNI).
  • SOLID — это trade-off: Каждая абстракция повышает расширяемость, но ухудшает читаемость кода, производительность процессора (виртуальные вызовы) и увеличивает расход памяти.
  • Где SOLID обязателен: Ядро сложной бизнес-логики (Core Domain), долгоживущие Enterprise-модули, публичные библиотеки.
  • Где SOLID вреден: Прототипы/MVP, одноразовые утилиты, Hot-Path участки с высокими требованиями к задержкам (Ultra-Low Latency).
  • Альтернатива OCP: В Java 21 sealed-иерархии со switch предпочтительнее полиморфизма OCP для закрытых наборов типов.

Чего избегать (Red Flags):

  • ❌ Утверждать: «Я всегда и везде на 100% следую всем принципам SOLID» (выдает догматика без опыта коммерческих компромиссов).
  • ❌ Заявлять, что абстракции и интерфейсы ничего не стоят с точки зрения JVM (память, GC, деоптимизация C2).
  • ❌ Создавать пары Interface + InterfaceImpl для каждого класса в проекте.

Связанные темы: