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

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

Патерн — це не готова бібліотека і не готовий фрагмент коду, який можна скопіювати через Ctrl+C / Ctrl+V. Це архітектурна концепція, стратегія та креслення, що описує, як вибуду...


🟢 Junior Level

Патерн (шаблон) проектування — це перевірене часом, повторюване та задокументоване архітектурне рішення типової задачі в об’єктно-орієнтованому програмуванні.

Патерн — це не готова бібліотека і не готовий фрагмент коду, який можна скопіювати через Ctrl+C / Ctrl+V. Це архітектурна концепція, стратегія та креслення, що описує, як вибудувати взаємозв’язки між класами та інтерфейсами для досягнення гнучкості та простоти супроводу коду.

graph LR
    A["Типова проблема проектування"] --> B["Патерн (Архітектурне креслення)"]
    B --> C["Адаптація під контекст задачі"]
    C --> D["Гнучкий, розширюваний код"]

Наочна побутова аналогія

Уявіть будівництво заміського будинку: Вам не потрібно заново вигадувати, як влаштувати стрічковий фундамент, двосхилий дах або систему опалення. Існують стандартні інженерні проекти та будівельні регламенти (патерни). Ви берете типовий проект фундаменту й адаптуєте його під розміри та тип ґрунту вашої конкретної ділянки.

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

  1. Економія часу: Ви використовуєте готове, налагоджене світовою спільнотою рішення, заощаджуючи години на проектуванні архітектури.
  2. Єдиний словник команди (Ubiquitous Language): Замість пояснення на пів години «давайте створимо клас із приватним конструктором і статичною точкою доступу» розробник каже: «Використаємо Singleton» або «Огорнемо в Decorator».
  3. Запобігання архітектурним помилкам: Патерни побудовані на базових принципах об’єктно-орієнтованого дизайну (SOLID).

Найпростіший приклад (Патерн Singleton)

// Класичний Singleton: гарантує наявність рівно одного екземпляра класу
public class DatabaseConnectionPool {

    // Статичне поле, що зберігає єдиний екземпляр
    private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool();

    // Приватний конструктор забороняє створення об'єкта через 'new' ззовні
    private DatabaseConnectionPool() {
        System.out.println("Ініціалізація пулу з'єднань...");
    }

    // Глобальна статична точка доступу
    public static DatabaseConnectionPool getInstance() {
        return INSTANCE;
    }
}

🟡 Middle Level

Ієрархія рівнів абстракції в розробці

У професійній інженерії важливо відрізняти патерни проектування від суміжних понять:

Рівень абстракції Опис Приклади в екосистемі Java
Ідіоми мови (Idioms) Специфічні для конкретної мови синтаксичні конструкції try-with-resources, Optional, Records, Pattern Matching
GoF-патерни Патерни проектування класів та їхньої взаємодії (Gang of Four) Strategy, Decorator, Observer, Factory Method
Enterprise-патерни Архітектура взаємодії з базами даних та шарами бізнес-логіки Repository, Unit of Work, Data Mapper, DTO
Cloud / Distributed Патерни розподілених систем та мікросервісів Circuit Breaker, Saga, CQRS, Outbox, Sidecar

[!NOTE] GoF (Gang of Four — «Банда чотирьох»): Еріх Гамма, Річард Хелм, Ральф Джонсон та Джон Вліссідіс — автори фундаментальної книги «Design Patterns: Elements of Reusable Object-Oriented Software» (1994), які описали 23 класичні патерни.

3 класичні категорії патернів GoF

  1. Породжувальні (Creational): Керують процесами створення об’єктів, ізолюючи клієнтський код від деталей інстанціювання (Singleton, Factory Method, Abstract Factory, Builder, Prototype).
  2. Структурні (Structural): Визначають способи компонування класів та об’єктів у більші й гнучкіші структури (Adapter, Decorator, Proxy, Facade, Composite, Bridge, Flyweight).
  3. Поведінкові (Behavioral): Організовують ефективний і гнучкий розподіл обов’язків та алгоритмів між взаємодіючими об’єктами (Strategy, Observer, Command, Template Method, Iterator, State, Chain of Responsibility, Visitor, Mediator, Memento).

Патерни GoF всередині стандартної бібліотеки Java (JDK)

Патерни оточують Java-розробника щодня безпосередньо в java.base:

// 1. Decorator: каскадне загортання потоків введення-виведення
InputStream is = new BufferedInputStream(new FileInputStream("data.bin"));

// 2. Factory Method: статичні фабричні методи колекцій
List<String> list = List.of("A", "B", "C");

// 3. Strategy: передача алгоритму порівняння
Collections.sort(users, Comparator.comparing(User::getAge));

// 4. Adapter: адаптація масиву до інтерфейсу List
List<String> adaptedList = Arrays.asList("Alpha", "Beta");

// 5. Prototype: метод clone() та інтерфейс Cloneable

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

З появою лямбда-виразів, Stream API, Records та зіставлення зі зразком (Pattern Matching) багато великовагових патернів GoF перетворилися на компактні мовні конструкції:

1. Патерн Strategy через Функціональні Інтерфейси

// ❌ До Java 8: створення інтерфейсу та десятка класів-реалізацій
// public interface DiscountStrategy { BigDecimal apply(BigDecimal price); }
// public class BlackFridayDiscount implements DiscountStrategy { ... }

// ✅ Modern Java: використання вбудованого Function / UnaryOperator
public BigDecimal calculatePrice(BigDecimal originalPrice, UnaryOperator<BigDecimal> discountStrategy) {
    return discountStrategy.apply(originalPrice);
}

// Виклик в один рядок через лямбду або Method Reference:
BigDecimal finalPrice = calculatePrice(price, p -> p.multiply(BigDecimal.valueOf(0.8)));

2. Заміна Visitor через Sealed Interfaces + Pattern Matching (Java 17–21)

Класичний патерн Visitor вимагав двосторонньої диспетчеризації (accept() / visit()) для обходу гетерогенних ієрархій. У Modern Java це елегантно вирішується через sealed interface та вираз switch:

public sealed interface OrderEvent permits OrderCreated, OrderPaid, OrderCancelled {}
public record OrderCreated(Long orderId) implements OrderEvent {}
public record OrderPaid(Long orderId, BigDecimal amount) implements OrderEvent {}
public record OrderCancelled(Long orderId, String reason) implements OrderEvent {}

// Обробка без громіздкого Visitor:
public void handle(OrderEvent event) {
    switch (event) {
        case OrderCreated c   -> System.out.println("Створено: " + c.orderId());
        case OrderPaid p      -> System.out.println("Оплачено: " + p.amount());
        case OrderCancelled r -> System.out.println("Скасовано: " + r.reason());
    }
}

🔴 Senior Level

Патерни як інструмент дотримання принципів SOLID

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

  • Open/Closed Principle (OCP): Патерни Strategy, Decorator та Observer дозволяють розширювати функціонал системи новими класами без модифікації наявного перевіреного коду.
  • Dependency Inversion Principle (DIP): Factory Method, Abstract Factory та Dependency Injection змушують високорівневі модулі залежати від абстракцій (інтерфейсів), а не від конкретних класів реалізації.
  • Single Responsibility Principle (SRP): Патерн Proxy відокремлює нефункціональні вимоги (логування, транзакції, кешування) від чистої бізнес-логіки.

Вплив поліморфізму патернів на JIT-компілятор (HotSpot JVM)

Під час проектування критичних до затримок ділянок коду (Hot-Path / Highload) Senior-інженер зобов’язаний враховувати, як патерни впливають на JIT-оптимізації JVM:

  1. Monomorphic Call Site (1 реалізація):
    Якщо інтерфейс патерну Strategy у рантаймі завжди представлений лише одним конкретним класом, JIT-компілятор C2 виконує агресивний інлайнінг (Method Inlining). Накладні витрати абстракції дорівнюють абсолютному нулю (код методу вбудовується безпосередньо в точку виклику).
  2. Bimorphic Call Site (2 реалізації):
    JIT генерує швидкий умовний перехід if (type == A) ... else if (type == B) ....
  3. Megamorphic Call Site (> 2 реалізацій):
    Інлайнінг неможливий. JVM змушена при кожному виклику здійснювати пошук адреси методу в таблиці віртуальних методів (vtable lookup), що руйнує конвеєр процесора і спричиняє скидання інструкцій кешу.
Оптимізація для Hot-Path:
Якщо варіантів стратегії обмежена кількість (3–5 штук), заміна поліморфних класів 
на enum зі switch-виразом дозволяє JIT генерувати компактну таблицю переходів (tableswitch) 
на рівні машинного коду процесора, випереджаючи інтерфейсний виклик.

Патерни в IoC-контейнерах (Spring Framework)

У корпоративних застосунках управління патернами майже повністю делеговане фреймворку:

  • Singleton & Prototype: Життєвий цикл бінів за замовчуванням контролюється контейнером Spring IoC (@Scope("singleton") / @Scope("prototype")). Ручна реалізація класичного Singleton у коді вважається антипатерном, оскільки блокує Unit-тестування.
  • Dynamic Proxy: Усі анотації @Transactional, @Cacheable, @Async та @Retryable під капотом побудовані на патерні Proxy (що реалізується через JDK Dynamic Proxy або ByteBuddy/CGLIB).

Небезпеки зловживання: Golden Hammer та Overengineering

                    ┌────────────────────────────┐
                    │       Проста задача        │
                    └─────────────┬──────────────┘
                                  │
                  ┌───────────────┴───────────────┐
                  ▼                               ▼
       Прагматичний інженер             Інженер-overengineer
       ┌────────────────────────┐       ┌─────────────────────────┐
       │ Проста функція         │       │ AbstractFactory         │
       │ + if-else              │       │ StrategyRegistry        │
       │ 15 рядків коду         │       │ ValidationProxy         │
       │ Читається за 30 секунд │       │ 12 інтерфейсів і класів │
       │                        │       │ Тиждень на багфіксинг   │
       └────────────────────────┘       └─────────────────────────┘
  1. Антипатерн «Золотий молоток» (Golden Hammer): Розробник, який щойно прочитав книгу GoF, намагається вбудувати улюблений патерн у кожен рядок коду, ускладнюючи найпростіший CRUD.
  2. Збільшення рівнів непрямості (Indirection Layers): Надмірне дроблення на фабрики та адаптери робить Stack Trace глибиною в 60 викликів, ускладнюючи зневадження та розуміння логіки.
  3. Принцип YAGNI (You Aren’t Gonna Need It): Введення патернів «на майбутнє» до того, як система реально зіткнулася з варіативністю бізнес-вимог, вважається архітектурною помилкою.

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

Обов’язково знати (Перші 30 секунд відповіді)

  • Визначення: Патерн проектування — це перевірене типове архітектурне рішення частої задачі в ООП, що описує відносини між класами та інтерфейсами.
  • 3 групи GoF (23 патерни):
    1. Породжувальні (Creational): створення об’єктів (Builder, Factory Method, Singleton).
    2. Структурні (Structural): композиція та адаптація (Adapter, Decorator, Proxy).
    3. Поведінкові (Behavioral): алгоритми та взаємодія (Strategy, Observer, Command).
  • Modern Java: У версіях 8–21 багато патернів замінено лямбдами (Strategy), Records та Sealed Interfaces (Visitor), а життєвий цикл об’єктів віддано під контроль Spring IoC.
  • Принцип прагматизму: Найкраща архітектура — найпростіша з можливих. Не використовуйте патерни там, де достатньо звичайної умови if-else.

Часті уточнюючі запитання на співбесіді

  1. «У чому різниця між архітектурним патерном та патерном проектування?»
    Відповідь: Архітектурний патерн (Microservices, Event-Driven, Hexagonal, CQRS) визначає глобальну структуру всієї системи або розподілених сервісів. Патерн проектування (GoF: Strategy, Factory) вирішує локальні задачі взаємодії класів у межах одного процесу JVM.

  2. «Чому класичний Singleton з книги GoF часто вважають антипатерном у Java?»
    Відповідь: Класичний Singleton зі статичним методом приховує залежності класу (порушує DIP), жорстко зв’язує код, вкрай ускладнює Unit-тестування (неможливо підмінити моком без важких інструментів на зразок PowerMock) і порушує багатопоточну безпеку без належної синхронізації. У сучасному коді життєвим циклом одинака повинен керувати DI-контейнер (Spring).

  3. «Як лямбда-вирази в Java 8 спростили патерн Strategy?»
    Відповідь: Раніше вимагалося створювати інтерфейс стратегії та окремий клас для кожної реалізації. У Java 8 будь-яка стратегія — це об’єкт стандартного функціонального інтерфейсу (Function, Consumer, Predicate, Supplier), що передається у вигляді лаконічної лямбди без створення зайвих файлів класів.

  4. «Що таке мегаморфний виклик (Megamorphic Call) і як патерни впливають на JIT?»
    Відповідь: Якщо інтерфейс патерну має понад дві активні реалізації в точці виклику, JIT-компілятор C2 не може виконати інлайнінг методу і змушений робити дорогий пошук адреси в таблиці віртуальних методів (vtable), що знижує пропускну здатність на викликах Hot-Path.

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

  • ❌ «Я завжди проектував систему на основі патернів ще до написання першого рядка коду» — явна ознака схильності до передчасного оверінжинірингу. Патерни вводяться в процесі рефакторингу в міру ускладнення бізнес-логіки.
  • ❌ «Патерн — це готовий шматок коду або бібліотека, яку можна підключити» — патерн є архітектурним кресленням і концепцією, а не бібліотечним кодом.
  • ❌ «Використання патернів завжди прискорює роботу програми» — патерни додають рівні непрямості та поліморфні виклики, що може викликати невеликі накладні витрати за CPU та пам’яттю.

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