Питання 1 · Розділ 2

Що таке патерни проектування?

GoF (Gang of Four — «банда чотирьох»: Gamma, Helm, Johnson, Vlissides, автори книги «Design Patterns», 1994).

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Патерни проектування — це зафіксовані в книгах і статтях рішення, які багаторазово застосовувались у реальних проєктах і довели свою працездатність.

Навіщо: не потрібно проєктувати рішення з нуля. Економія 2-5 годин на завдання, бо патерн вже відлагоджений і зрозумілий іншим розробникам.

Проста аналогія: Уявіть, що ви будуєте будинок. Вам не потрібно щоразу придумувати, як зробити дах чи фундамент — є типові проєкти, які вже довели свою ефективність. Патерни — це такі “типові проєкти” для коду.

Навіщо потрібні:

  • Не винаходити велосипед
  • Використовувати рішення, які вже працюють
  • Спілкуватися з колегами однією мовою (“тут потрібен Singleton”)

Приклад:

// Патерн Singleton — тільки один екземпляр класу
public class Logger {
    private static Logger instance = new Logger();
    private Logger() {}
    public static Logger getInstance() { return instance; }

    public void log(String message) {
        System.out.println(message);
    }
}

// Використання
Logger.getInstance().log("Hello!");

Коли використовувати:

  • Коли стикаєтеся з типовою проблемою
  • Коли код стає складним і заплутаним
  • Коли потрібно зробити код зрозумілішим для інших розробників

🟡 Middle Level

Рівні патернів

| Рівень | Приклади | Де застосовується | | —————— | ————————————– | ———————- | | Ідіоми мови | Try-with-resources, Optional, Streams | Синтаксис Java | | GoF патерни | Strategy, Decorator, Observer, Factory | Взаємодія класів | GoF (Gang of Four — «банда чотирьох»: Gamma, Helm, Johnson, Vlissides, автори книги «Design Patterns», 1994). | Enterprise | Repository, Unit of Work, Data Mapper | Робота з даними | | Cloud/Distributed | Circuit Breaker, Saga, CQRS | Мікросервіси |

Основні категорії

1. Породжуючі (Creational)

  • Допомагають створювати об’єкти
  • Приклади: Singleton, Factory, Builder
  • Навіщо: Приховати процес створення об’єктів

2. Структурні (Structural)

  • Допомагають компонувати класи та об’єкти
  • Приклади: Adapter, Decorator, Proxy
  • Навіщо: Зробити сумісними різні інтерфейси

3. Поведінкові (Behavioral)

  • Описують взаємодію між об’єктами
  • Приклади: Strategy, Observer, Chain of Responsibility
  • Навіщо: Керувати алгоритмами та передачею даних

Патерни в Modern Java

Багато класичних патернів спростилися з приходом Java 8-21:

// ❌ Класична Strategy (багато класів)
public interface PaymentStrategy { void pay(); }
public class CreditCardStrategy implements PaymentStrategy { ... }
public class PayPalStrategy implements PaymentStrategy { ... }

// ✅ Modern Java (лямбди)
public void processPayment(UnaryOperator<BigDecimal> paymentStrategy) {
    BigDecimal result = paymentStrategy.apply(amount);
}

// Використання
processPayment(amount -> amount.subtract(discount));

Типові помилки

  1. Золотий молоток (Golden Hammer)
    // ❌ Використовувати патерн всюди, де знаємо
    // Створення Abstract Factory для одного об'єкта
    
    // ✅ Використовувати коли реально потрібно
    // Простий if-else краще за непотрібну абстракцію
    
  2. Overengineering
    • Занадто багато рівнів абстракції
    • Складно читати і відлагоджувати
    • Stack trace на 50 рівнів
  3. Карго-культ
    • Сліпе копіювання без розуміння
    • Реалізація Cloneable замість конструктора копіювання

Коли НЕ використовувати патерни

  1. Прості CRUD-додатки — прямий код читабельніший за абстракції
  2. Прототипи на вихідні — швидкість важливіша за архітектуру
  3. Коли патерн не вирішує вашу проблему — не підганяйте задачу під патерн

🔴 Senior Level

Патерн як архітектурний контракт

На глибокому рівні патерн визначає:

  1. Розподіл відповідальності (Separation of Concerns)
    • За що відповідають об’єкти
    • Чіткі межі між компонентами
  2. Інтерфейси взаємодії
    • Мінімізація залежностей (Loose Coupling)
    • Контракти між модулями
  3. Точки розширення
    • Де система може бути доповнена без зміни наявного коду
    • Open/Closed Principle на практиці

Еволюція патернів у Modern Java (8-21+)

Strategy / Command → Лямбди:

// Було: 10+ класів
// Стало: Function<T, R>, Consumer<T>, Predicate<T>

Template Method → Композиція:

// Замість наслідування — передача лямбд у конструктор
public class Processor {
    private final Function<Data, Result> transformer;

    public Processor(Function<Data, Result> transformer) {
        this.transformer = transformer;
    }
}

Singleton / Prototype → DI-контейнер:

// Spring керує життєвим циклом
@Component  // Singleton scope за замовчуванням
public class UserService { }

@Scope("prototype")
public class OrderProcessor { }

Proxy → Spring AOP:

// @Transactional, @Cacheable — все це Proxy
// JDK Dynamic Proxy або CGLIB під капотом
@Transactional
public void processOrder() { }

Архітектурні Trade-offs

Що виграємо:

  • ✅ Гнучкість і розширюваність
  • ✅ Тестованість
  • ✅ Зрозумілість для тих, хто знає патерн
  • ✅ Слабка зв’язаність

Що втрачаємо:

  • ❌ Збільшення кількості класів
  • ❌ Рівні опосередкування (indirection)
  • ❌ Складність відлагодження
  • ❌ Накладні витрати на продуктивність

Megamorphic Calls та JIT

// Патерни впливають на продуктивність JVM!

// Monomorphic call (1 реалізація) → інлайнинг
PaymentStrategy strategy = new CreditCard();
strategy.pay();  // JIT вбудовує код → 0 накладних витрат

// Bimorphic call (2 реалізації) → все ще швидко
// Megamorphic call (>2 реалізації) → непрямий виклик через vtable
// → Вимірюване просідання по CPU в Hot-path!

Оптимізація:

// Для критичних шляхів: enum замість інтерфейсу
public enum PaymentType {
    CREDIT_CARD {
        @Override public void pay() { /* код */ }
    },
    PAYPAL {
        @Override public void pay() { /* код */ }
    };
    public abstract void pay();
}
// → JIT краще оптимізує enum switch

Патерни у розподілених системах

Cloud-Native патерни:

Circuit Breaker → відмовостійкість
Saga → розподілені транзакції
Sidecar → інфраструктура поруч із сервісом
CQRS → розділення читання та запису
Event Sourcing → журнал подій замість стану

Взаємозв’язок патернів:

Abstract Factory (Creational)
  → повертає Proxy (Structural)
    → який огортає бізнес-логіку
      → керовану через Strategy (Behavioral)

Chain of Responsibility (Behavioral)
  → збирається через Builder (Creational)

Facade (Structural)
  → спрощує інтерфейс до підсистеми
    → керованої через Strategy (Behavioral)

Принцип достатності

Найкращий патерн — той, що вирішує задачу мінімальним кодом:

// ❌ Overengineering
public interface MessageValidator { boolean validate(Message m); }
public class EmailValidator implements MessageValidator { ... }
public class PhoneValidator implements MessageValidator { ... }
public class ValidatorFactory { ... }
public class ValidationStrategy { ... }

// ✅ Достатньо
public boolean isValid(Message m) {
    return m.getType() == EMAIL
        ? emailRegex.matcher(m.getContent()).matches()
        : phoneRegex.matcher(m.getContent()).matches();
}

// Патерн потрібен ТІЛЬКИ якщо:
// - Планується розширення (нові типи валідації)
// - Складна логіка в кожній гілці
// - Потрібно тестувати окремо

Production Experience

Реальний сценарій #1: Overengineering убив читабельність

  • Проєкт: 15 патернів на 10 000 рядків коду
  • Проблема: новий розробник витрачає 2 тижні на онбординг
  • Рішення: спростили до 5 основних патернів
  • Результат: онбординг 3 дні, менше багів

Реальний сценарій #2: Pattern misuse у Spring

  • Ручний Singleton замість @Component
  • Проблема: неможливо замокати в тестах
  • Рішення: делегували Spring IoC
  • Результат: тестованість + гнучкість

Best Practices

  1. Знайте патерни, але не використовуйте без необхідності
  2. Modern Java спрощує багато патернів (лямбди, Records)
  3. DI-контейнер замінює ручні Singleton/Prototype/Factory
  4. Композиція > Наслідування — гнучкіше і менш крихке
  5. Простота — якщо if-else достатньо, не ускладнюйте
  6. Документуйте — чому обрали цей патерн
  7. Перевіряйте — чи не порушує патерн SOLID
  8. Думайте про JIT — мегаморфні виклики впливають на CPU

Резюме для Senior

  • Патерни — інструмент боротьби зі складністю, не ціль
  • Modern Java робить багато патернів “невидимими” (лямбди, Records)
  • DI-контейнер — ваш Singleton/Factory/Prototype менеджер
  • Trade-offs: гнучкість vs читабельність, розширюваність vs складність
  • JIT оптимізації: monomorphic > megamorphic calls
  • Принцип достатності: найкращий патерн — мінімальний код
  • Розподілені системи — нові патерни (Saga, Circuit Breaker)
  • Патерни мають реагувати на реальну потребу, а не теоретичну

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

Обов’язково знати:

  • Патерни — перевірені рішення типових проблем, економлять 2-5 годин на задачу
  • Три категорії: Creational (створення), Structural (композиція), Behavioral (взаємодія)
  • GoF (Gang of Four) — Gamma, Helm, Johnson, Vlissides, автори книги 1994 року
  • Modern Java (8-21) спрощує багато патернів: лямбди замінюють Strategy, DI замінює Singleton
  • Принцип достатності: найкращий патерн — мінімальний код, якщо if-else достатньо — не ускладнюйте
  • Overengineering убиває читабельність: 15 патернів на 10K рядків — це антипатерн
  • Композиція предпочтительніша за наслідування — гнучкіше і менш крихке
  • Megamorphic calls (>2 реалізації) впливають на JIT оптимізації та CPU в hot-path

Часті уточнювальні запитання:

  • Коли НЕ використовувати патерни? — Прості CRUD, прототипи, патерн не вирішує проблему
  • Чим ідіома відрізняється від патерну? — Ідіома специфічна мові (try-with-resources), патерн універсальний
  • Які cloud-native патерни ви знаєте? — Circuit Breaker, Saga, CQRS, Event Sourcing, Sidecar
  • Що таке Golden Hammer? — Використання улюбленого патерну всюди, де тільки можна

Червоні прапорці (НЕ говорити):

  • “Я використовую патерни всюди, де знаю” — це overengineering
  • “Singleton вирішує всі проблеми” — Singleton вважається антипатерном у багатьох випадках
  • “Патерни завжди покращують продуктивність” — часто навпаки, додають overhead
  • “Я не використовую патерни, пишу простіше” — незнання базових архітектурних рішень

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

  • [[2. Які категорії патернів існують]] — докладна класифікація
  • [[16. Які антипатерни ви знаєте]] — погані рішення, які шкодять коду
  • [[7. В чому різниця між Factory Method та Abstract Factory]] — породжуючі патерни
  • [[10. Коли використовувати Strategy]] — поведінкові патерни на практиці
  • [[12. В чому перевага Decorator перед наслідуванням]] — композиція vs наслідування