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

Какие антипаттерны противоречат SOLID принципам

Каждый из пяти принципов SOLID был сформулирован как прямое противоядие от конкретной группы архитектурных антипаттернов.


🟢 Junior Level

Антипаттерны (Anti-patterns) — это типичные, регулярно повторяющиеся ошибки проектирования архитектуры, которые на первый взгляд кажутся простыми и быстрыми, но в долгосрочной перспективе делают кодовую базу хрупкой, нетестируемой и дорогой в поддержке.

Каждый из пяти принципов SOLID был сформулирован как прямое противоядие от конкретной группы архитектурных антипаттернов.

5 главных антипаттернов против SOLID

  1. God Object / The Blob (Божественный объект) — нарушает SRP:
    • Класс-гигант (2000+ строк), который делает всё: сохраняет в БД, проверяет входные данные, шлет email и считает баланс.
  2. Hardcoded Conditionals / Magic Switch — нарушает OCP:
    • Монолитные методы с гигантскими цепочками if (type == "CARD") ... else if (type == "SBP") .... Добавление новой фичи требует хирургического вмешательства в проверенный код.
  3. Refusal of Bequest (Отказ от наследства) — нарушает LSP:
    • Подкласс наследует базовый класс, но для половины методов выбрасывает throw new UnsupportedOperationException("Not supported") или оставляет пустые заглушки.
  4. Fat Interface (Жирный интерфейс) — нарушает ISP:
    • Интерфейс на 40 методов. Классы вынуждены реализовывать десятки чужих методов, которые им никогда не понадобятся.
  5. Hard Dependency / Static Cling — нарушает DIP:
    • Прямое создание инфраструктурных зависимостей через оператор new внутри бизнес-методов (new PostgresConnection()), делающее невозможным изолированное тестирование.

Мнемоника STUPID: Полный антагонист SOLID

В индустрии популярна мнемоника STUPID, описывающая вредные привычки процедурного стиля в ООП:

  • S (Singleton): Злоупотребление синглтонами с глобальным мутабельным состоянием.
  • T (Tight Coupling): Жесткое зацепление классов без интерфейсов.
  • U (Untestability): Невозможность написать модульный тест без поднятия всей базы и внешних сервисов.
  • P (Premature Optimization): Преждевременная оптимизация в ущерб читаемости.
  • I (Indistinct Names): Размытые бессмысленные имена (Manager, DataProcessor, Helper).
  • D (Duplication): Дублирование логики (нарушение DRY).

🟡 Middle Level

Подробная карта соответствия: Принцип SOLID против Антипаттерна

Буква SOLID Защищаемый инвариант Противоречащие антипаттерны Симптомы в коде
S (SRP) У класса одна причина для изменения (один актор). God Object (The Blob)
Feature Envy
Accidental Duplication
Метод одного класса постоянно читает геттеры другого класса; LCOM4 $\ge 2$; класс импортирует одновременно SQL, Mail и UI.
O (OCP) Открытость для расширения, закрытость для правок. Type Code with Switch
Speculative Generality
Постоянные изменения одних и тех же файлов в Git; либо обратная крайность — 15 интерфейсов без единой альтернативной реализации.
L (LSP) Подкласс взаимозаменяем с базовым типом. Refusal of Bequest
Square-Rectangle Trap
Broken Contract
UnsupportedOperationException; скрытое искажение инвариантов при сеттерах; нарушение контракта equals/hashCode.
I (ISP) Клиент зависит только от того, что использует. Fat Interface
Header Interface
Interface Pollution
Интерфейс механически дублирует методы класса (IUserService); пустые методы-заглушки в реализациях.
D (DIP) Высокий уровень зависит от абстракций. Static Cling
Service Locator
Circular Dependency
new Service() внутри класса; ServiceLocator.getBean(); взаимные ссылки сервисов A -> B -> A.

Анализ антипаттерна Feature Envy (Зависть к чужим функциям)

Один из самых коварных запахов кода по Мартину Фаулеру:

// Антипаттерн Feature Envy: метод класса OrderProcessor "завидует" классу Wallet
public class OrderProcessor {
    public void processPayment(User user, BigDecimal amount) {
        // Метод лезет во внутренности User и Wallet, производя расчеты над чужими данными!
        if (user.getWallet().getBalance().compareTo(amount) >= 0) {
            BigDecimal newBalance = user.getWallet().getBalance().subtract(amount);
            user.getWallet().setBalance(newBalance);
        } else {
            throw new InsufficientFundsException();
        }
    }
}

Почему это нарушает SRP и инкапсуляцию: Логика управления балансом принадлежит кошельку (Wallet), а не процессору заказов. Исправление (Принцип Tell, Don’t Ask):

user.getWallet().withdraw(amount); // Кошелек сам проверяет баланс и списывает средства

🔴 Senior Level

Глубокий анализ: Circular Dependencies и прокси-ловушки Spring

Антипаттерн Circular Dependency (Циклическая зависимость) — прямое следствие игнорирования DIP и SRP:

@Service
public class OrderService {
    private final PaymentService paymentService;
}

@Service
public class PaymentService {
    private final OrderService orderService; // ЦИКЛ!
}

Технические последствия в JVM:

  1. Срыв Constructor Injection: Spring не может построить направленный ациклический граф (DAG) создания бинов и выбрасывает фатальный BeanCurrentlyInCreationException.
  2. Костыль @Lazy и деградация CGLIB: Если разработчик ставит @Lazy, Spring внедряет динамический CGLIB-прокси.
    • Каждый вызов через такой прокси добавляет накладные расходы динамического интерцептора (~30–50 нс).
    • Возникает Proxy Self-Invocation Problem: вызов метода @Transactional внутри того же класса идет мимо прокси, и транзакция молча не открывается!
  3. Решение по Senior-стандарту: Разорвать цикл:
    • Либо выделить третьего координатора (Mediator).
    • Либо перейти на асинхронную шину событий (Domain Events через ApplicationEventPublisher).

Дилемма архитектуры: Anemic Domain Model vs Rich Domain Model

Один из самых ожесточенных споров в enterprise Java:

  • Anemic Domain Model (Мартин Фаулер): Сущности — чистые DTO с геттерами и сеттерами без логики. Вся бизнес-логика сосредоточена в классах Service.
    • Плюсы: Идеальная совместимость с JPA/Hibernate, простота для новичков.
    • Минусы: Нарушение инкапсуляции; сервисы быстро мутируют в неуправляемые God Objects.
  • Rich Domain Model (Богатая модель в DDD): Сущность сама инкапсулирует бизнес-логику и защищает свои инварианты (order.cancel(), order.applyDiscount()).
    • Плюсы: Соответствие SRP и High Cohesion; чистая бизнес-логика.
    • Минусы: Сложность интеграции с ORM-библиотеками, требующими пустых конструкторов и мутабельных полей.

Архитектурный крах: Big Ball of Mud и Stovepipe Systems

Когда антипаттерны не пресекаются на ранних этапах:

  1. Big Ball of Mud (Большой ком грязи): Система, в которой любая часть кода может неявно зависеть от любой другой части. Рефакторинг «в лоб» невозможен.
    • Лечение: Только изоляция фасадами и постепенная выкатка через Strangler Fig Pattern.
  2. Stovepipe Enterprise System (Система дымоходов): Несколько независимых монолитов внутри одной компании, дублирующих функции друг друга с несовместимыми форматами данных и протоколами (нарушение ISP и DIP на уровне масштаба предприятия).

4 Tricky Questions

1. Является ли паттерн Singleton безусловным антипаттерном в современной Java-разработке?

Ответ: Классический GoF Singleton (public static getInstance() с приватным статическим полем) в современном enterprise-коде признан антипаттерном. Он создает неявную глобальную зависимость, делает класс нетестируемым в многопоточных тестах (разделяемое изменяемое состояние между тестами) и нарушает DIP. Однако концепция синглтона на уровне жизненного цикла фреймворка (Singleton Scope в Spring) — это фундаментальный паттерн. В Spring бин создается в единственном экземпляре контейнером, внедряется через конструктор (DIP соблюден) и может быть легко подменен Mock-объектом в тестах.

2. Как антипаттерн Speculative Generality (Спекулятивная генерализация) связан с нарушением OCP?

Ответ: Спекулятивная генерализация — это «выворачивание OCP наизнанку». Разработчик, начитавшись про OCP, пытается предугадать все возможные изменения на 5 лет вперед и создает 10 слоев интерфейсов, абстрактных фабрик и адаптеров для функционала, у которого всегда будет ровно одна реализация. Это превращает код в переусложненный конструктор, повышает когнитивную нагрузку и нарушает принцип YAGNI. Настоящий OCP требует создания абстракций только в тех точках, где вариативность реально подтверждена требованиями бизнеса.

3. В чем разница между запахом кода Feature Envy и нарушением закона Деметры (Law of Demeter)?

Ответ:

  • Feature Envy (Зависть к функционалу): метод обращается к множеству полей или геттеров другого класса чаще, чем к своим собственным данным. Проблема в том, где живет метод: он вычисляет чужую логику снаружи.
  • Law of Demeter (Train Wreck): метод вызывает цепочку вызовов вглубь через несколько посредников (a.getB().getC().getD()). Проблема в навигации по закрытой структуре: метод знает структуру связей далеко за пределами своего прямого соседа. Оба антипаттерна лечатся переносом метода ближе к данным (Tell, Don’t Ask).

4. Почему антипаттерн Header Interface (интерфейс IUserService с одной реализацией UserServiceImpl) вреден?

Ответ:

  1. Ложное ощущение слабой связанности: интерфейс просто слепо повторяет все методы одного класса. При изменении логики сервиса интерфейс меняется синхронно с реализацией (абстракции нет).
  2. Засорение навигации в IDE: при переходе по вызову метода разработчик попадает в интерфейс вместо реального кода.
  3. Нарушение ISP: такой интерфейс со временем разрастается до 50 методов, и все клиенты вынуждены зависеть от единого широкого контракта. Создавать интерфейс следует только при появлении второй реализации или при необходимости изолировать модуль через Role Interface.

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

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

  • Главные антипаттерны:
    • Против SRP: God Object (The Blob), Feature Envy, Anemic Model.
    • Против OCP: Hardcoded Switch / Type Code, Speculative Generality.
    • Против LSP: Refusal of Bequest (UnsupportedOperationException), мутации сторон в Квадрат/Прямоугольник.
    • Против ISP: Fat Interface, Header Interface (механический IService).
    • Против DIP: Hardcoded new, Service Locator, Circular Dependencies.
  • STUPID мнемоника: Singleton, Tight Coupling, Untestability, Premature Optimization, Indistinct Names, Duplication.
  • Опасность циклических зависимостей: Spring решает их через @Lazy CGLIB-прокси со штрафом к задержке и риском потери транзакционности.

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

  • ❌ Защищать GoF Singleton с глобальным статическим методом getInstance() (признак легаси-мышления).
  • ❌ Считать, что создание пары Interface / Impl для каждого класса спасает от антипаттернов (это антипаттерн Header Interface).
  • ❌ Игнорировать проблему Feature Envy (выспрашивание чужих полей вместо инкапсулированного вызова метода).

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