Антипаттерны

Антипаттерн — повторяющееся решение, которое выглядит удобным локально, но регулярно создаёт предсказуемые проблемы. Это не ярлык для любого кода, который мне не нравится: нужны контекст, наблюдаемые последствия и более подходящая альтернатива.

God Object

Один класс знает и делает слишком много:

controller
+ validation
+ SQL
+ HTTP clients
+ mapping
+ notifications

Признаки:

  • изменение любой функции затрагивает класс;
  • слишком много dependencies;
  • tests требуют настроить всю систему;
  • методы работают с несвязанными группами полей.

Разделение идёт по use cases и ответственности, а не просто по числу строк.

Big Ball of Mud

У системы нет читаемых границ зависимостей. Любой модуль обращается к любому, database tables становятся общим API, а локальное изменение вызывает неизвестные эффекты.

Лечить это «полной переписью» рискованно. Сначала выделяют наблюдаемую границу: package/module/API, тестируют её и постепенно уменьшают обходные связи.

Service Locator

Класс сам получает dependency из глобального context:

PaymentClient client =
        Context.getBean(PaymentClient.class);

Зависимость не видна в constructor, а unit test зависит от container. Для обязательных collaborators предпочитаю constructor injection.

Primitive Obsession

Разные сущности представлены одинаковыми String/long, поэтому compiler не различает их:

void transfer(long source, long target, BigDecimal value)

Value objects делают контракт явнее:

void transfer(AccountId source, AccountId target, Money value)

Shotgun Surgery

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

Перед рефакторингом ищу все реализации и формулирую единый инвариант. Механически объединить код в «Utils» недостаточно.

Golden Hammer

Знакомый инструмент используется для любой задачи: Kafka для синхронного запроса, inheritance для всякого расширения, microservice для маленького автономного модуля.

Проверка — сравнить хотя бы одно более простое решение и назвать требование, ради которого нужна дополнительная сложность.

Swallowed exception

Ошибка логируется и превращается в успешное завершение:

try {
    process();
} catch (Exception exception) {
    log.error("failed", exception);
}

В транзакции это может привести к commit частичного результата. Реальный пример: Кейс исключение было проглочено внутри транзакции.

Distributed Monolith

Сервисы развёрнуты отдельно, но должны обновляться вместе, используют общую схему БД и образуют длинную синхронную цепочку. Появилась сетевая сложность без независимости.

Как разбираю подозрение на антипаттерн

  1. Какой конкретный симптом наблюдается?
  2. Какое решение его создаёт?
  3. В каком контексте решение всё-таки допустимо?
  4. Какой минимальный шаг уменьшит проблему?
  5. Чем проверю, что стало лучше?

Вопросы для собеседования

  1. Любой Singleton — антипаттерн? Нет; проблема чаще в скрытом global state и неясном lifecycle.
  2. Большой класс автоматически God Object? Нет, важнее число несвязанных ответственностей.
  3. Почему Service Locator осложняет тест? Dependencies скрыты и получаются из global container.
  4. Как исправлять Big Ball of Mud? Постепенно вводить проверяемые границы, а не начинать слепой rewrite.

Связанные заметки