Вопрос 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]]