🧩 Розділ 2 · Питання #16

Які антипатерни ви знаєте

Один клас бере на себе надто багато відповідальностей (сотні полів, тисячі рядків коду), перетворюючи решту класів системи на пасивні структури збереження даних:


🟢 Junior Level

Антипатерн (Anti-pattern) — це типовий, часто повторюваний підхід до вирішення задачі, який здається інтуїтивно простим або правильним, але на практиці призводить до негативних наслідків: ускладнення підтримки, прихованих багів, деградації продуктивності та швидкого зростання технічного боргу. На відміну від звичайної помилки кодингу, антипатерн має усталену структуру, впізнавану назву та задокументоване правильне рішення (рефакторинг).


Побутова аналогія

  • Самолікування антибіотиками при застуді: Здається швидким «універсальним» засобом подолати будь-яке нездужання, але в довгостроковій перспективі знищує корисну мікрофлору і виробляє небезпечну резистентність бактерій.
  • Ремонт труби, що протікає, скотчем: Вирішує проблему на 10 хвилин, але маскує першопричину аварії та гарантує масштабний потоп у найближчому майбутньому.

Базові антипатерни в коді

1. God Object (Божественний об’єкт / The Blob)

Один клас бере на себе надто багато відповідальностей (сотні полів, тисячі рядків коду), перетворюючи решту класів системи на пасивні структури збереження даних:

// ❌ АНТИПАТЕРН: Клас робить абсолютно все
public class OrderManager {
    public void validateCart() { /* валідація кошика */ }
    public void calculateDiscounts() { /* розрахунок знижок */ }
    public void processPayment() { /* списання коштів */ }
    public void generatePdfInvoice() { /* генерація PDF-рахунку */ }
    public void sendEmailConfirmation() { /* відправка email */ }
    public void backupToDatabase() { /* запис у БД */ }
}

// ✅ РІШЕННЯ: Розподіл за принципом Single Responsibility (SRP)
// PaymentService, InvoiceGenerator, NotificationService, OrderRepository

2. Spaghetti Code (Спагеті-код)

Код із заплутаним потоком керування, надмірною вкладеністю умов if-else, нескінченними циклами та повною відсутністю модульної структури. Читання та модифікація такого коду перетворюються на болісний процес.

3. Copy-Paste Programming (Програмування копіюванням)

Дублювання однакових фрагментів коду по всій системі замість створення перевикористовуваних методів, функцій або утилітарних класів:

  • Небезпека: Баг, виправлений в одному місці, залишається в 10 інших скопійованих блоках коду (явище «Shotgun Surgery»).

4. Magic Numbers & Strings (Магічні числа та рядки)

Використання безіменних числових літералів і текстових констант безпосередньо в бізнес-умовах:

// ❌ АНТИПАТЕРН: Що означають 86400 та рядок "ACTIVE"?
if (user.getStatus().equals("ACTIVE") && duration > 86400) { /* логіка */ }

// ✅ РІШЕННЯ: Іменовані константи та Enum
private static final int SECONDS_IN_DAY = 86_400;
if (user.getStatus() == UserStatus.ACTIVE && duration > SECONDS_IN_DAY) { /* логіка */ }

🟡 Middle Level

Антипатерни об’єктно-орієнтованого проєктування

1. Golden Hammer (Золотий молоток / Закон інструменту)

«Якщо єдиний інструмент у руках — молоток, будь-яка проблема видається цвяхом».

Примусове натягування улюбленої технології або патерна проєктування на всі задачі поспіль без урахування реального контексту:

  • Приклади: Використання Apache Kafka для передачі 3 повідомлень на добу; мікросервісна архітектура для пет-проєкту з 10 користувачами; впровадження патерна Visitor там, де достатньо звичайного поліморфізму.

2. Poltergeist (Полтергейст / Циганський віз)

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

// ❌ АНТИПАТЕРН: Зайвий клас-пустушка
public class OrderProcessorProxyHelper {
    public void execute(Order order) {
        new RealOrderProcessor().process(order);
    }
}

3. Sequential Coupling (Послідовне зв’язування)

Клас спроєктований так, що його методи зобов’язані викликатися в строго визначеній часовій послідовності, але це обмеження не зафіксовано в системі типів і перевіряється лише під час виконання (runtime):

// ❌ АНТИПАТЕРН: Неочевидний прихований порядок виклику методів
ReportGenerator report = new ReportGenerator();
report.initTemplate();
report.loadData();
report.renderPdf(); // Якщо забути викликати initTemplate() -> NullPointerException!

// ✅ РІШЕННЯ: Builder, Fluent API або передача параметрів через конструктор
ReportGenerator report = ReportGenerator.fromTemplate(template).withData(data);
report.renderPdf();

4. Anemic Domain Model (Анемічна доменна модель)

Сутності бізнес-логіки (User, Account, Order) являють собою прості «мішки з даними» — чисті гетери й сетери без жодного бізнес-методу. Уся бізнес-логіка винесена у велетенські сервісні класи (UserService, OrderService):

  • Чому це антипатерн (за Мартіном Фаулером): Порушується інкапсуляція ООП, правила валідації розмиваються по десятках сервісів, неможливо гарантувати цілісність інваріантів сутності.
  • Альтернатива: Rich Domain Model (Багата доменна модель) у парадигмі Domain-Driven Design (DDD), де сама сутність контролює зміни свого стану (order.cancel(), account.withdraw(amount)).

5. Lava Flow (Потік лави / Мертвий код)

Шари застарілого, мертвого або недописаного коду, який усі бояться чіпати або видаляти («Воно якось працює, зачепиш — усе впаде»). Виникає через відсутність автоматизованих тестів і часту зміну розробників у команді.


Зведена таблиця архітектурних симптомів

Антипатерн Симптом / Ознака Наслідки Спосіб лікування
God Object Клас > 1000 рядків, десятки ін’єкцій Merge-конфлікти, неможливість unit-тестування Декомпозиція за принципом SRP, фасад
Sequential Coupling Виклики методів вимагають неявного порядку Runtime-винятки IllegalStateException Builder, Fluent API, Factory Method
Golden Hammer Надмірно складний стек під просту задачу Зростання витрат на інфраструктуру, оверінжиніринг Принципи KISS, YAGNI, аналіз trade-offs
Anemic Model Сервіси з логікою, Entity лише з гетерами Розмазування інваріантів, спагеті в сервісах Rich Domain Model (DDD)
Lava Flow Закоментований код, застарілі @Deprecated класи Засмічення репозиторію, страх перед рефакторингом Покриття інтеграційними тестами, видалення мертвого коду

🔴 Senior Level

Архітектурні антипатерни в Enterprise та Spring

1. Big Ball of Mud (Велика купа бруду)

Архітектура застосунку повністю деградувала: межі між шарами стерті, контролери напряму викликають репозиторії в обхід сервісів, присутні циклічні залежності між пакетами:

  • Інструменти автоматичного контролю в CI/CD: ArchUnit — написання модульних тестів на дотримання архітектурних правил:
    import com.tngtech.archunit.junit.AnalyzeClasses;
    import com.tngtech.archunit.junit.ArchTest;
    import com.tngtech.archunit.lang.ArchRule;
    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
    
    @AnalyzeClasses(packages = "com.myapp")
    public class ArchitectureTest {
        @ArchTest
        public static final ArchRule no_controllers_in_repositories = 
            noClasses().that().resideInAPackage("..repository..")
                       .should().dependOnClassesThat().resideInAPackage("..controller..");
    }
    

2. Cargo Cult Programming (Програмування культу Карго) в екосистемі Spring

Бездумне копіювання анотацій, конфігурацій або архітектурних шаблонів «бо так скрізь роблять»:

  • @Transactional на кожному методі: Розміщення анотації на простих методах читання SELECT або методах, що виконують довгі мережеві HTTP-виклики. Це утримує з’єднання з пулу HikariCP відкритим протягом усього зовнішнього запиту, спричиняючи вичерпання пулу з’єднань (Pool Exhaustion).
  • Field Injection (@Autowired private MyService service;): Порушує інкапсуляцію, робить клас нетестованим без підняття контексту Spring чи рефлексії, провокує виникнення циклічних залежностей. Єдиний стандарт — Constructor Injection.
  • Безконтрольний @Async без явного TaskExecutor: За замовчуванням Spring використовує SimpleAsyncTaskExecutor, який не перевикористовує пули потоків, а створює новий потік операційної системи на кожну задачу. При сплеску навантаження це негайно призводить до OutOfMemoryError: unable to create new native thread.

3. Distributed Monolith (Розподілений моноліт)

Система формально розбита на десятки мікросервісів, але вони пов’язані жорсткими синхронними ланцюжками HTTP/gRPC викликів та мають спільну базу даних:

  • Проблеми: Каскадні падіння (один сервіс, що впав, зупиняє весь ланцюжок), розподілені транзакції (2PC/Saga), пекло релізів (неможливо розгорнути сервіс А без одночасного релізу сервісів Б і В).
  • Рішення: Асинхронна взаємодія через брокери повідомлень (Kafka/RabbitMQ), ізоляція сховищ (Database-per-service), чітке дотримання Bounded Contexts.

4. Premature Optimization (Передчасна оптимізація)

«Передчасна оптимізація — корінь усіх бід у програмуванні» (Дональд Кнут).

Ускладнення коду низькорівневими багатопотоковими блокуваннями, самописними пулами об’єктів та бітовими операціями до проведення реальних навантажувальних тестів і профілювання (async-profiler, JProfiler):

  • Результат: Код стає неможливим для підтримки, а сучасний JIT-компілятор (C2) втрачає здатність ефективно оптимізувати переускладнені конструкції.

Метрики якості коду та статичний аналіз

Для автоматичного виявлення антипатернів у виробничих пайплайнах застосовують інструменти статичного аналізу: SonarQube, SpotBugs, Checkstyle, PMD.

Ключові вимірювані метрики:

  1. Cyclomatic Complexity (Цикломатична складність Маккейба): Кількість лінійно незалежних шляхів виконання в коді. Поріг для окремого методу: $\le 10$.
  2. Cognitive Complexity (Когнітивна складність): Наскільки важко людському мозку зрозуміти логіку потоку керування (вкладені цикли та умови збільшують метрику експоненціально).
  3. Instability ($I$) пакета: $I = \frac{C_e}{C_a + C_e}$, де $C_e$ (Efferent Coupling) — вихідні залежності, а $C_a$ (Afferent Coupling) — вхідні залежності. Метрика відображає стійкість модуля до зовнішніх змін.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь (Elevator Pitch)

«Антипатерн — це контрпродуктивний шаблон проєктування, рішення з передбачувано негативними наслідками для системи. На рівні коду основними антипатернами є God Object (порушення SRP), Sequential Coupling (неявний порядок викликів), Magic Values та Spaghetti Code. На архітектурному рівні — Golden Hammer (натягування однієї технології на будь-які задачі), Cargo Cult (сліпе копіювання анотацій на зразок @Transactional без розуміння механіки), Anemic Domain Model (позбавлення сутностей бізнес-поведінки) та Distributed Monolith (мікросервіси з жорсткою синхронною зв’язаністю). Захист базується на принципах SOLID, KISS, YAGNI, автоматизованому контролі через ArchUnit та статичному аналізі (SonarQube)».


3–4 каверзних запитання з відповідями

  1. Чому патерн Singleton часто називають класичним антипатерном?
    • Відповідь: Класичний синглтон приховує залежності класу всередині коду методів (getInstance()) замість їх явного оголошення в сигнатурі конструктора. Це порушує принцип інверсії залежностей (DIP), унеможливлює повноцінне модульне тестування (неможливо підставити мок-об’єкт), зберігає глобальний змінний стан і провокує race conditions у багатопотоковому коді. У сучасних системах Singleton реалізується не через приватний конструктор і статичні методи, а через життєвий цикл біна в DI-контейнері (Spring Singleton Scope).
  2. Чим небезпечний Cargo Cult навколо анотації @Transactional на методі відправки email?
    • Відповідь: Якщо розмістити мережевий виклик (відправка пошти через SMTP або виклик зовнішнього платіжного API) всередині методу з @Transactional, з’єднання з базою даних утримуватиметься відкритим протягом усього часу мережевого очікування (таймаути зовнішніх API можуть сягати десятків секунд). Це миттєво вичерпає пул з’єднань БД (HikariCP pool exhaustion), заблокує рядки в таблицях і спричинить відмову всієї системи. Мережеві виклики зобов’язані виконуватися поза транзакціями бази даних (через Outbox pattern, TransactionSynchronization або після підтвердження транзакції).
  3. Що таке Anemic Domain Model і чому Мартін Фаулер назвав її антипатерном?
    • Відповідь: Це архітектурний стиль, за якого сутності предметної області позбавлені поведінки та містять лише дані (поля, гетери й сетери), а вся бізнес-логіка винесена в сервісний шар. Фаулер назвав це антипатерном, оскільки такий підхід суперечить фундаментальній ідеї ООП — поєднанню даних і операцій над ними в єдиному об’єкті. Це призводить до розмиття бізнес-правил валідації по різних сервісах і втрати гарантій інваріантів сутності.
  4. Як бібліотека ArchUnit допомагає запобігти деградації архітектури в Big Ball of Mud?
    • Відповідь: ArchUnit дозволяє описувати архітектурні правила у вигляді звичайних модульних Java-тестів, які виконуються під час збірки в CI/CD конвеєрі. Наприклад, можна заборонити циклічні залежності між пакетами (slices().matching("..myapp.(*)..").should().beFreeOfCycles()), заборонити контролерам напряму викликати репозиторії в обхід сервісів, або зобов’язати всі сервіси мати суфікс Service. Якщо розробник порушить встановлені архітектурні межі, білд упаде на етапі виконання тестів.

Типові помилки та Red Flags

  • 🚩 Твердження: «Я проставляю @Transactional над кожним методом сервісу про всяк випадок» (найгрубіший маркер Cargo Cult програмування).
  • 🚩 Виправдання гігантських сервісів на 3000 рядків фразою «Зате весь код зібраний в одному місці» (повне нерозуміння God Object та принципу SRP).
  • 🚩 Використання Field Injection (@Autowired полів) у новому коді замість впровадження через конструктор.
  • 🚩 Ігнорування метрик статичного аналізу та відсутність архітектурних тестів (ArchUnit) у великих enterprise-проєктах.

Пов’язані теми