Що таке патерни проектування?
GoF (Gang of Four — «банда чотирьох»: Gamma, Helm, Johnson, Vlissides, автори книги «Design Patterns», 1994).
🟢 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));
Типові помилки
- Золотий молоток (Golden Hammer)
// ❌ Використовувати патерн всюди, де знаємо // Створення Abstract Factory для одного об'єкта // ✅ Використовувати коли реально потрібно // Простий if-else краще за непотрібну абстракцію - Overengineering
- Занадто багато рівнів абстракції
- Складно читати і відлагоджувати
- Stack trace на 50 рівнів
- Карго-культ
- Сліпе копіювання без розуміння
- Реалізація Cloneable замість конструктора копіювання
Коли НЕ використовувати патерни
- Прості CRUD-додатки — прямий код читабельніший за абстракції
- Прототипи на вихідні — швидкість важливіша за архітектуру
- Коли патерн не вирішує вашу проблему — не підганяйте задачу під патерн
🔴 Senior Level
Патерн як архітектурний контракт
На глибокому рівні патерн визначає:
- Розподіл відповідальності (Separation of Concerns)
- За що відповідають об’єкти
- Чіткі межі між компонентами
- Інтерфейси взаємодії
- Мінімізація залежностей (Loose Coupling)
- Контракти між модулями
- Точки розширення
- Де система може бути доповнена без зміни наявного коду
- 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
- Знайте патерни, але не використовуйте без необхідності
- Modern Java спрощує багато патернів (лямбди, Records)
- DI-контейнер замінює ручні Singleton/Prototype/Factory
- Композиція > Наслідування — гнучкіше і менш крихке
- Простота — якщо if-else достатньо, не ускладнюйте
- Документуйте — чому обрали цей патерн
- Перевіряйте — чи не порушує патерн SOLID
- Думайте про 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 наслідування