Що таке делегування в ООП
@Transactional створює проксі, який делегує виклики реальному біну, огортаючи в транзакцію. Оверхед ~50-200ns на виклик, але дає декларативну транзакційність.
🟢 Junior Level
Делегування — це механізм, при якому об’єкт не виконує завдання сам, а доручає його іншому об’єкту, зберігаючи відповідальність за результат. Це як коли менеджер доручає завдання розробнику, але залишається відповідальним перед клієнтом.
Проста аналогія: Ви хочете замовити піцу. Замість того щоб готувати самим (наслідування), ви телефонуєте в піцерію (делегування). Ви — інтерфейс, піцерія — реалізація.
Приклад делегування:
// Делегат — виконує роботу
public class Printer {
public void print(String text) {
System.out.println("Printing: " + text);
}
}
// Делегатор — доручає роботу
public class DocumentProcessor {
private final Printer printer; // Делегат
public DocumentProcessor(Printer printer) {
this.printer = printer;
}
public void processAndPrint(String doc) {
// обробка документу
printer.print(doc); // Делегування друку
}
}
Коли використовувати:
- Коли потрібно перевикористати код без наслідування
- Коли потрібна можливість змінювати реалізацію в runtime
- Для реалізації патернів Strategy, State, Decorator
🟡 Middle Level
Як це працює
Делегування = об’єкт (Delegator) перенаправляє виклики іншому об’єкту (Delegate). В Java це найчастіше Method Forwarding — явний виклик методу делегата.
Різниця з простим викликом: При “чистому” делегуванні делегат може мати посилання на контекст делегатора (передача this). В Java найчастіше зустрічається просте перенаправлення.
Практичне застосування
Приклад з JDK — Collections.synchronizedList:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// SynchronizedList делегує ArrayList, додаючи synchronized
Kotlin: вбудоване делегування:
class MyList(inner: List<String>) : List<String> by inner
// Компілятор генерує всі методи-делегати автоматично
Делегування vs Проксі
| Характеристика | Делегування | Проксі |
|---|---|---|
| Зв’язок | Статичний (код) | Динамічний (runtime) |
| Реалізація | Явні методи | java.lang.reflect.Proxy / CGLIB (бібліотека для генерації bytecode-підкласів проксі) |
| Продуктивність | Швидше (прямий виклик методу — JVM може заинлайнити, ~0.3ns) | Повільніше (reflection через java.lang.reflect.Proxy, ~100-500ns) |
| Гнучкість | Фіксована | Можна міняти на льоту |
Типові помилки
| Помилка | Рішення |
|---|---|
| Circular delegation (A→B→A) → StackOverflow | Архітектурний аналіз |
| Делегат без доступу до private полів | Передача контексту через параметри |
| Ручне написання 20 методів-делегатів | Використовувати IDE Generate / Lombok |
Коли НЕ варто використовувати делегування
- Прості утиліти (static методи краще)
- Коли оверхед на boilerplate перевищує користь
- Критичні до продуктивності ділянки (low latency)
🔴 Senior Level
Internal Implementation на рівні JVM
Method Forwarding (перенаправлення методів) — bytecode:
INVOKEVIRTUAL Printer.print(Ljava/lang/String;)V
→ invokevirtual → VTable lookup (пошук методу у віртуальній таблиці класу) → Printer.print()
З final полем:
private final Printer printer;
→ monomorphic call → JIT inlines → прямий виклик (0.3ns)
JIT Optimization: Якщо поле-делегат final, JIT-компілятор може заінлайнити код делегата (вбудувати тіло методу прямо в місце виклику, прибираючи оверхед виклику повністю). Це називається monomorphic inline caching — JVM бачить один тип і оптимізує.
Архітектурні Trade-offs
Ручне делегування:
- ✅ Плюси: Compile-time safety, no reflection overhead, IDE support
- ❌ Мінуси: Boilerplate, maintenance burden
Dynamic Proxy:
- ✅ Плюси: Zero boilerplate, runtime flexibility
- ❌ Мінуси: Reflection overhead (~10-100x slower), no compile-time checks
Edge Cases
- Circular Delegation: A→B→A → StackOverflowError
- Рішення: Архітектурні тести (ArchUnit), явний заборон
- Visibility Limitation: Делегат не бачить private полів делегатора
- Рішення: Передача контексту через параметри, package-private access
- Long Delegation Chains: A→B→C→D → складно відлагоджувати
- Рішення: Limit to 2-3 hops, monitoring через stack traces
Продуктивність
| Підхід | Latency | Примітка |
|---|---|---|
| Direct call | ~0.3 ns | Базовий |
| Final delegate | ~0.3 ns | JIT inlined |
| Non-final delegate | ~1-3 ns | Monomorphic |
| JDK Proxy | ~100-500 ns | Reflection |
| CGLIB Proxy | ~50-200 ns | Bytecode gen |
- Inlining: final → 0 overhead
- Memory: +16 bytes per delegate object header
Thread Safety
- Stateless delegates: Thread-safe за замовчуванням
- Stateful delegates: Потрібна синхронізація
- Concurrent delegation: Можна використовувати різні delegate instances per thread
class ConcurrentProcessor {
private final ThreadLocal<Delegate> delegate =
ThreadLocal.withInitial(Delegate::new);
// Один delegate на потік — немає конкуренції
}
Production Experience
Spring AOP (Aspect-Oriented Programming — аспектно-орієнтоване програмування, дозволяє додавати cross-cutting concerns на кшталт транзакцій без зміни бізнес-коду) через делегування:
@Transactional створює проксі, який делегує виклики реальному біну, огортаючи в транзакцію. Оверхед ~50-200ns на виклик, але дає декларативну транзакційність.
Реальний кейс: Migration з JDBC на JPA — виділили DataAccessDelegate, переключили делегат без зміни бізнес-логіки. Результат: zero-downtime migration.
Monitoring
ArchUnit (бібліотека для архітектурних тестів — перевіряє залежності між класами на рівні junit-тестів):
@ArchTest
static void no_circular_delegation = slices()
.matching("*(..)")
.should().notCycleDependency();
SonarQube (статичний аналізатор коду — знаходить code smells, баги, уразливості): Max delegation chain length → налаштування порогу
Best Practices for Highload
- final delegates для inlining
- Stateless delegates для thread-safety
- Max 2-3 hops для відлагоджуваності
- Generate delegates через IDE/Lombok для boilerplate
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Делегування = об’єкт доручає завдання іншому, зберігаючи відповідальність за результат
- Method Forwarding — явний виклик методу делегата, швидше ніж Dynamic Proxy
finalделегат → JIT inlines → 0 overhead (~0.3 ns), non-final → monomorphic (~1-3 ns)- JDK Proxy (~100-500 ns) vs CGLIB Proxy (~50-200 ns) vs ручне делегування (~0.3 ns)
- Circular delegation (A→B→A) → StackOverflowError — потрібен архітектурний контроль
- Spring AOP
@Transactional— проксі делегує виклики реальному біну, оверхед ~50-200ns
Часті уточнюючі запитання:
- Делегування vs Проксі? — Делегування = статичне (compile-time), Проксі = динамічне (runtime через рефлексію)
- Як уникнути Circular Delegation? — ArchUnit тести на цикли залежностей, явний заборон
- Чому
finalважливий для делегата? — JIT бачить один тип → monomorphic inline caching → повний inlining - Делегування і Thread Safety? — Stateless делегати thread-safe за замовчуванням; ThreadLocal для stateful
Червоні прапори (НЕ говорити):
- “Делегування завжди повільніше прямого виклику” (з
finalполем — той самий ~0.3 ns після inlining) - “Dynamic Proxy — найкращий вибір завжди” (100-500ns overhead, в hot path це критично)
- “Довгі ланцюжки делегування — це нормально” (Max 2-3 hops, інакше складно відлагоджувати)
Пов’язані теми:
- [[10. Що таке композиція і наслідування]]
- [[11. У яких випадках краще використовувати композицію замість наслідування]]
- [[4. Як рефакторити код, що порушує принцип Open_Closed]]
- [[3. Що таке принцип Open_Closed]]