Питання 12 · Розділ 18

Що таке делегування в ООП

@Transactional створює проксі, який делегує виклики реальному біну, огортаючи в транзакцію. Оверхед ~50-200ns на виклик, але дає декларативну транзакційність.

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

🟢 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

  1. Circular Delegation: A→B→A → StackOverflowError
    • Рішення: Архітектурні тести (ArchUnit), явний заборон
  2. Visibility Limitation: Делегат не бачить private полів делегатора
    • Рішення: Передача контексту через параметри, package-private access
  3. 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]]