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

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

При цьому для зовнішнього клієнта об'єкт-делегатор продовжує виступати відповідальним виконавцем.


🟢 Junior Level

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

При цьому для зовнішнього клієнта об’єкт-делегатор продовжує виступати відповідальним виконавцем.

Життєва аналогія

Керівник проєкту та програміст:

  • Клієнт звертається до керівника проєкту: «Додайте в мобільний застосунок оплату через Apple Pay».
  • Керівник не пише код сам: він перенаправляє (делегує) технічне завдання розробнику.
  • Проте для клієнта інтерфейсом залишається керівник: він приймає завдання і повертає готовий результат.

Простий приклад делегування в Java

// 1. Делегат (реальний виконавець)
public class SmsSender {
    public void send(String phone, String text) {
        System.out.println("Відправка SMS на " + phone + ": " + text);
    }
}

// 2. Делегатор (координатор)
public class NotificationManager {
    // Внутрішній об'єкт-делегат
    private final SmsSender smsSender;

    public NotificationManager(SmsSender smsSender) {
        this.smsSender = smsSender;
    }

    public void notifyCustomer(String phone, String message) {
        // Додаткова валідація або логування
        System.out.println("Логуємо відправку повідомлення...");

        // ДЕЛЕГУВАННЯ виконання
        smsSender.send(phone, message);
    }
}

Навіщо потрібне делегування:

  • Альтернатива наслідуванню: замість створення складної та жорсткої ієрархії класів ми комбінуємо готові об’єкти.
  • Гнучкість: об’єкт-делегат можна легко замінити в конструкторі або через сетер у рантаймі.

🟡 Middle Level

Тонка відмінність: True Delegation проти Forwarding (Переадресації)

В академічній теорії ООП (Генрі Ліберман, 1986; GoF) прийнято суворо розрізняти два терміни, які в повсякденній Java-розробці часто об’єднують словом «делегування»:

Критерій Forwarding (Переадресація) True Delegation (Істинне делегування)
Передача контексту (this) Ні. Об’єкт A просто викликає b.operation(). Усередині методу operation() посилання this вказує на об’єкт B. Так. Об’єкт A передає посилання на себе: b.operation(this). Усередині B контекстом виконання залишається A.
Реалізація в Java 99% стандартного Java-коду (композиція, патерн Wrapper). Патерни Visitor, State, Strategy, де стратегії явно передається контекст власника.
Проблема this (The Self-Problem) Відсутня. Методи B ізольовані від A. Присутня: якщо B викликає свої власні перевизначувані методи, виникає плутанина з поліморфізмом.

Делегування в стандартній бібліотеці JDK

Делегування лежить в основі багатьох класів Java Core:

  1. java.util.Collections.synchronizedList(list): Клас SynchronizedList реалізує інтерфейс List, зберігає всередині приватний final List<E> list та огортає кожен виклик у блок синхронізації:
    public int size() {
        synchronized (mutex) { return list.size(); } // Делегування з синхронізацією
    }
    
  2. java.io.InputStream та декоратори: BufferedInputStream та GZIPInputStream наслідують FilterInputStream, який повністю делегує читання байтів внутрішньому потоку in.read(), додаючи буферизацію чи розпакування.

Делегування vs Dynamic Proxy (JDK / ByteBuddy)

Характеристика Явне делегування (Compile-time) Динамічний проксі (Runtime Proxy)
Реалізація Пишеться вручну (або генерується Lombok @Delegate). Генерується на льоту (java.lang.reflect.Proxy, CGLIB, ByteBuddy).
Типобезпека Сувора перевірка компілятором Java. Помилки конфігурації часто виявляються лише в Runtime.
Продуктивність Швидкість прямого виклику (0.3–1 ns при інлайнінгу JIT). Накладні витрати рефлексії та інтерцепторів (30–150 ns).
Призначення Архітектурна декомпозиція бізнес-логіки. Наскрізна функціональність (Spring AOP, @Transactional, Security).

🔴 Senior Level

Проблема селф-диспетчеризації (The Self-Problem Лібермана)

У мовах із нативною підтримкою делегування (Self, JavaScript прототипи) виклик методу делегата зберігає вихідний контекст this. У Java симуляція істинного делегування наштовхується на фундаментальне обмеження:

public class Window {
    private final Rectangle bounds;

    public Window(Rectangle bounds) {
        this.bounds = bounds;
    }

    public int getArea() {
        return bounds.getArea(this); // Передаємо себе як контекст
    }

    public int getBorderWidth() { return 2; }
}

public class Rectangle {
    private int w, h;
    public int getArea(Window context) {
        // Делегат змушений опитувати делегатора про його властивості
        return (w + context.getBorderWidth()) * (h + context.getBorderWidth());
    }
}

Якщо забути передати this, внутрішня логіка делегата оперуватиме власним станом, втрачаючи поліморфні перевизначення делегатора.


Аналіз JIT-компіляції HotSpot (C2) та Devirtualization

Скептики часто стверджують: «Делегування створює зайвий рівень непрямості (Indirection) та уповільнює виконання через додаткові виклики функцій».

На рівні асемблерного коду HotSpot JVM це не так:

  1. Monomorphic Call Site Inlining: Якщо поле-делегат оголошене як private final Processor delegate, компілятор C2 виконує Class Hierarchy Analysis (CHA). Виявивши рівно одну активну реалізацію методу, JIT виконує Method Inlining: машинний код методу делегата вбудовується прямо в точку виклику делегатора. Накладні витрати виклику перетворюються на нуль наносекунд!
  2. Bimorphic Inline Caching: Якщо в рантаймі чергуються 2 типи делегата (наприклад, FastSmsSender та BackupSmsSender), JIT будує швидку conditional-перевірку (cmp klass; jne fallback) та інлайнить тіла обох методів.
  3. Megamorphic Fallback: Якщо типів делегата $\ge 3$, JIT переходить до пошуку за таблицею інтерфейсів itable. У цьому випадку непрямий перехід коштує ~10–15 тактів CPU.

Делегування в сучасних мовах: Kotlin vs Java + Lombok

  • У Kotlin делегування вбудоване в синтаксис мови через оператор by:
    class FastList<T>(private val inner: List<T>) : List<T> by inner
    // Компілятор автоматично генерує всі 25 методів-делегатів інтерфейсу List!
    
  • У Java для усунення багатослівного boilerplate-коду застосовують бібліотеку Lombok:
    public class CustomCollection<E> implements Collection<E> {
        @Delegate
        private final Collection<E> internalStorage = new ArrayList<>();
        // Lombok на етапі компіляції генерує методи-делегати
    }
    

4 Tricky Questions

1. Що станеться при виникненні циклічного делегування (Circular Delegation) і як його запобігти?

Відповідь: Якщо об’єкт A у процесі обробки делегує виклик об’єкту B, а B у своїй логіці знову викликає метод об’єкта A, віртуальна машина входить у нескінченну рекурсію. Це призводить до переповнення стека викликів і фатальної помилки java.lang.StackOverflowError. Запобігання:

  1. Архітектурне правило односпрямованості: залежності повинні утворювати спрямований ациклічний граф (DAG).
  2. Автоматичний контроль у CI: тести ArchUnit (slices().matching("..").should().notCycleDependency()), що забороняють циклічні зв’язки між пакетами та класами.

2. Чи є патерн Decorator різновидом делегування?

Відповідь: Так. Декоратор (Decorator) — це спеціалізована форма делегування, об’єднана зі спільним інтерфейсом:

  1. Декоратор реалізує той самий інтерфейс, що й обгортаний об’єкт (Decorator implements Component).
  2. Декоратор зберігає посилання на екземпляр Component у полі.
  3. Усі стандартні методи інтерфейсу він делегує цьому екземпляру, але до або після делегування додає власну прозору поведінку (кешування, аудит, шифрування).

3. Як делегування впливає на потокобезпеку (Thread-Safety), якщо делегатор і делегат мають власний стан?

Відповідь: Якщо делегатор синхронізує виклики на своєму моніторі (synchronized(this)), це не захищає внутрішній стан делегата, якщо на нього є пряме посилання в інших потоків. Для забезпечення потокобезпеки:

  • Делегат повинен бути повністю інкапсульований усередині делегатора (жодних гетерів назовні, захисне копіювання в конструкторі).
  • Або сам делегат має бути потокобезпечним (наприклад, AtomicInteger, ConcurrentHashMap).
  • Якщо делегат stateless (не має змінних полів), делегування є потокобезпечним за замовчуванням.

4. У чому небезпека використання анотації Lombok @Delegate в enterprise-проєктах?

Відповідь: Анотація @Delegate сліпо делегує всі публічні методи типу поля. Це породжує дві серйозні проблеми:

  1. Порушення Interface Segregation Principle (ISP) та витік API: клас ненавмисно виставляє назовні десятки непотрібних або небезпечних методів делегата (наприклад, методи мутації внутрішньої колекції).
  2. Крихкість збірки: під час оновлення сторонньої бібліотеки делегата в його інтерфейсі можуть з’явитися нові методи, сигнатури яких конфліктують із власними методами класу-делегатора, що призведе до помилок компіляції. Безпечніше використовувати ручну генерацію методів-делегатів засобами IDE (IntelliJ IDEA: Generate -> Delegate Methods) з вибором лише необхідних методів.

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

Головні тези (30 секунд на відповідь):

  • Суть: Об’єкт перенаправляє виконання завдання іншому об’єкту-помічнику, залишаючись точкою входу для клієнта.
  • Основа патернів: Базовий будівельний блок для композиції та патернів GoF (Strategy, State, Decorator, Adapter).
  • Forwarding vs True Delegation: У Java практично завжди використовується Forwarding (без передачі this). True Delegation вимагає передачі this у метод помічника.
  • Продуктивність: private final Delegate delegate повністю інлайниться JIT-компілятором C2 до рівня прямого машинного коду (0 накладних витрат).

Чого уникати (Red Flags):

  • ❌ Стверджувати, що делегування завжди повільніше за наслідування (при мономорфному inlining JIT різниця дорівнює нулю).
  • ❌ Допускати цикли взаємного делегування (ризик StackOverflowError).
  • ❌ Виставляти посилання на внутрішній змінюваний делегат через гетер (витік інкапсуляції).

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