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

Що таке композиція і наслідування

White-box reuse — видні нутрощі батька (наслідування). Black-box reuse — тільки публічний інтерфейс (композиція). Devirtualization — JIT замінює віртуальний виклик на прямий. Mo...

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

🟢 Junior Level

Композиція і наслідування — це два основних способи організації зв’язків між класами в ООП. Наслідування створює відношення “є” (IS-A): один клас стає приватним випадком іншого. Композиція створює відношення “має” (HAS-A): один об’єкт містить інший об’єкт як частину.

Проста аналогія: Наслідування — як отримання рис від батьків (ви є людиною). Композиція — як покупка телефону (ви маєте телефон, можете його замінити).

Приклад наслідування (IS-A):

public class Animal {
    public void eat() { System.out.println("Eating..."); }
}

public class Dog extends Animal {  // Dog IS-A Animal
    public void bark() { System.out.println("Woof!"); }
}

Приклад композиції (HAS-A):

public class Engine {
    public void start() { System.out.println("Engine started"); }
}

public class Car {
    private final Engine engine;  // Car HAS-A Engine

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();  // Делегування
    }
}

Коли використовувати:

  • Наслідування: коли є реальна ієрархія типів (Dog IS-A Animal)
  • Композиція: у більшості інших випадків — це гнучкіший підхід

🟡 Middle Level

Як це працює

Наслідування реалізує зв’язок “є” (IS-A) і використовує механізм таблиць віртуальних методів (VTable) на рівні JVM. Нащадок отримує доступ до protected полів батька — це “білий ящик” (White-box reuse).

Композиція реалізує зв’язок “має” (HAS-A) через виклики методів іншого об’єкта. Взаємодія тільки через публічний інтерфейс — це “чорний ящик” (Black-box reuse).

White-box reuse — видні нутрощі батька (наслідування). Black-box reuse — тільки публічний інтерфейс (композиція). Devirtualization — JIT замінює віртуальний виклик на прямий. Monomorphic = один тип виклику → inlining. Megamorphic = 3+ типи → пошук по таблиці.

Практичне застосування

Чому Effective Java каже “Favor Composition over Inheritance”:

Критерій Наслідування Композиція
Зв’язок Статичний (compile-time) Динамічний (runtime)
Інкапсуляція Порушується (видні protected поля) Зберігається
Гнучкість Не можна змінити батька Можна підмінити компонент
Тестованість Складно мокати Легко підмінити на mock
Крихкість Зміна батька ламає всіх Ізольовані зміни

Типові помилки

Помилка Рішення
Наслідування заради перевикористання коду Використовувати композицію + делегування
Глибока ієрархія (5+ рівнів) Рефакторинг в сторону композиції
ArrayListMySpecialList через наслідування MySpecialList з полем List всередині

Альтернативи наслідуванню

  1. Strategy: замість SmsNotification extends NotificationNotificationService з полем SenderStrategy
  2. Decorator: обгортання об’єкта для додавання функцій (логування, кешування)
  3. Delegation: клас реалізує інтерфейс, але викликає методи делегата

Коли НЕ варто використовувати композицію

  • Чисті ієрархії типів (винятки: IOExceptionSocketException)
  • Фреймворки з Template Method патерном
  • Коли потрібен поліморфізм і робота через спільний базовий тип

Template Method vs Strategy: Template Method = ви контролюєте алгоритм, підкласи — кроки. Strategy = клієнт обирає весь алгоритм повністю. Template Method — коли скелет алгоритму стабільний. Strategy — коли весь алгоритм може змінюватися.


🔴 Senior Level

Internal Implementation на рівні JVM

Наслідування та VTable: При виклику віртуального методу JVM використовує таблицю віртуальних методів (VTable). Кожен клас має свою VTable з вказівниками на реалізації методів. При глибокій ієрархії (5+ рівнів) JIT-компілятору складніше робити devirtualization та inlining.

// Наслідування: invokevirtual, пошук в VTable
class Parent { void foo() {} }
class Child extends Parent { @Override void foo() {} }
// parent.foo() → invokevirtual → VTable lookup

// Композиція з final: JIT може заинлайнити
class Service {
    private final Delegate delegate;  // final → monomorphic call
    void execute() { delegate.run(); } // JIT inlines → прямий виклик
}

Архітектурні Trade-offs

Наслідування:

  • ✅ Плюси: Поліморфізм з коробки, перевикористання коду, Template Method
  • ❌ Мінуси: Крихкий базовий клас, утечка реалізації, статичний зв’язок

Композиція:

  • ✅ Плюси: Динамічна поведінка, тестованість (легко мокати), слабка зв’язаність
  • ❌ Мінуси: Більше boilerplate коду (делегування), більше об’єктів в heap

Edge Cases

  1. Fragile Base Class Problem: зміна addAll у батька, який викликає add, ламає нащадка, що перевизначив обидва методи
    • Рішення: Композиція + Forwarding
    • Це деталь реалізації HashSet: addAll викликає add internally. Але покладатися на це не можна — в іншій реалізації може бути інакше. Саме тому наслідування від класів, не призначених для цього, крихке.
  2. Shared State при композиції: кілька делегатів працюють зі спільними даними
    • Рішення: Явна передача контексту або State Object
  3. Circular Delegation: A → B → A → StackOverflowError
    • Рішення: Архітектурний аналіз, ArchUnit правила

Продуктивність

Метрика Наслідування Композиція
Object size Всі поля батьків в одному об’єкті Окремі об’єкти + заголовки
Method call invokevirtual (VTable lookup) invokevirtual + possible inlining
JIT optimization Складніше при deep hierarchy Простіше з final полями
Memory ~16 byte header + всі поля ~16 bytes × N об’єктів + посилання
  • Inlining: JIT легко інлайнить final делегатів → оверхед ~0
  • VTable: При глибині 5+ рівнів megamorphic calls → деоптимізація

Thread Safety

  • Наслідування: synchronized на методі батька блокує весь об’єкт
  • Композиція: Можна використовувати різні lock’и для різних делегатів → finer-grained concurrency

Production Experience

Рефакторинг в enterprise проекті: NotificationService на 2000 рядків з нащадками EmailNotification, SmsNotification, PushNotification. Проблема: додавання нової стратегії вимагало зміни базового класу.

Рефакторинг: виділили NotificationStrategy інтерфейс, кожен тип — окрема реалізація. NotificationService став координатором. Результат: нові типи додаються без зміни існуючого коду (OCP).

Monitoring

ArchUnit правила:

@ArchTest
static void no_deep_inheritance = classes()
    .should().notHaveSimpleNameMatching(".*Impl.*")
    .andShould().haveLessThanNAncestors(3);

SonarQube: Depth of Inheritance Hierarchy > 5 → Code Smell

Best Practices for Highload

  • final делегати для кращого inlining
  • Package-private для внутрішніх компонентів
  • Sealed classes (Java 17+) для контрольованого наслідування

Sealed classes доступні з Java 17. Для Java 8/11 використовуйте final класи і документуйте, які класи призначені для наслідування.

  • Composition over Inheritance як default вибір

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

Обов’язково знати:

  • Наслідування = IS-A (статичний зв’язок, white-box reuse), Композиція = HAS-A (динамічна, black-box)
  • “Favor Composition over Inheritance” — Effective Java, Josh Bloch
  • Композиція зберігає інкапсуляцію, наслідування порушує (видні protected поля)
  • JIT легко інлайнить final делегатів → оверхед композиції ~0
  • Наслідування: крихкий базовий клас, утечка реалізації, статичний зв’язок
  • Fragile Base Class: зміна addAll у батька ламає нащадка, що перевизначив add
  • Sealed classes (Java 17+) для контрольованого наслідування

Часті уточнюючі запитання:

  • Чому композиція краща? — Динамічна заміна компонентів, тестованість (легко мокати), слабка зв’язаність
  • Коли наслідування допустиме? — Справжнє IS-A, фреймворки з Template Method, спільні інфраструктурні поля (BaseEntity)
  • Що таке devirtualization? — JIT замінює віртуальний виклик на прямий, коли знає точний тип
  • VTable overhead при наслідуванні? — 5+ рівнів → megamorphic calls → деоптимізація JIT

Червоні прапори (НЕ говорити):

  • “Наслідування — найкращий спосіб перевикористати код” (головна причина крихкості)
  • “Глибока ієрархія — ознака хорошого дизайну” (5+ рівнів — code smell)
  • “Композиція завжди краща” (є legit випадки наслідування: Template Method, IS-A)

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

  • [[11. У яких випадках краще використовувати композицію замість наслідування]]
  • [[12. Що таке делегування в ООП]]
  • [[5. Що таке принцип Liskov Substitution]]
  • [[6. Наведіть приклад порушення принципу Liskov Substitution]]