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

У яких випадках краще використовувати композицію замість наслідування

VTable — таблиця вказівників на методи. Monomorphic = JIT знає точний тип → inlining (~1 ns). Megamorphic = 3+ типи → пошук по таблиці (~10-20 ns).

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

🟢 Junior Level

Композиція переважніша за наслідування у більшості випадків. це одне з ключових правил з книги “Effective Java” Джошуа Блоха. Наслідування створює жорсткий зв’язок між класами, а композиція — гнучкий.

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

Проблема наслідування:

// Погано: наслідування заради перевикористання
public class MyHashSet extends HashSet<String> {
    private int addCount = 0;

    @Override
    public boolean add(String s) {
        addCount++;
        return super.add(s);
    }

    @Override
    public boolean addAll(Collection<? extends String> c) {
        addCount += c.size();  // ПОМИЛКА: addAll викликає add!
        return super.addAll(c); // Буде double count
    }
}

Рішення через композицію:

// Добре: композиція + делегування
public class CountingHashSet {
    private final Set<String> set = new HashSet<>();
    private int addCount = 0;

    public boolean add(String s) {
        addCount++;
        return set.add(s);
    }

    public int getAddCount() { return addCount; }
}

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

  • Коли потрібно перевикористати код, але немає відношення IS-A
  • Коли потрібна можливість змінювати поведінку в runtime
  • Коли батьківський клас не призначений для наслідування

🟡 Middle Level

Як це працює

Наслідування = White-box reuse: нащадок бачить нутрощі батька (protected поля і методи). Це створює сильну зв’язаність.

Композиція = Black-box reuse: взаємодія тільки через публічний інтерфейс. Делегат може бути замінений без зміни коду делегатора.

VTable — таблиця вказівників на методи. Monomorphic = JIT знає точний тип → inlining (~1 ns). Megamorphic = 3+ типи → пошук по таблиці (~10-20 ns).

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

Коли композиція перемагає:

1. Уникнення крихкості (Fragile Base Class)

// Stack наслідує Vector — порушення інкапсуляції
public class MyStack extends Vector<String> {
    public void push(String item) { addElement(item); }
    public String pop() { return removeElementAt(size() - 1); }
}
// Проблема: клієнт може викликати insertElementAt(0) → LIFO зламано!
// Stack передбачає LIFO (останній увійшов — перший вийшов).
// Але Vector дозволяє insertElementAt(0, item) — вставка на початок.
// Будь-який клієнт Vector може обійти LIFO-інваріант стека.

2. Динамічний поліморфізм

// Зміна стратегії на льоту
public class PaymentService {
    private PaymentProcessor processor;

    public void setProcessor(PaymentProcessor processor) {
        this.processor = processor;  // Runtime change
    }
}

3. Чистота API Композиція дозволяє виставити назовні тільки потрібні методи, сховавши внутрішні деталі.

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

Помилка Рішення
Наслідування від ArrayList для додавання функцій Композиція з List полем
Stack extends Vector — порушений інваріант Stack з внутрішнім List
Наслідування заради доступу до protected полів Передача даних через конструктор

Коли наслідування ДОПУСТИМЕ

Випадок Приклад Чому OK
IS-A Relationship Dog extends Animal Реальна ієрархія типів
Framework extension AbstractHttpMessageConverter Призначено для розширення
Logical grouping BaseEntity з id, createdAt Спільні поля для всіх сутностей

// BaseEntity — компроміс. Це не чистий SRP, але прагматизм: // всі сутності БД потребують id. Головне — щоб BaseEntity // не містив бізнес-логіки, тільки інфраструктурні поля.

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

  • Фреймворки з Template Method (призначені для наслідування)
  • Коли потрібен доступ до protected методів батька
  • Коли поліморфізм через базовий тип критичний

🔴 Senior Level

Internal Implementation на рівні JVM

VTable Overhead:

Наслідування:
  Object → A → B → C → D → E (5 рівнів)
  e.foo() → invokevirtual → VTable lookup → 5 pointer chases

Композиція з final:
  class Service { private final Delegate d; }
  s.d.run() → monomorphic call → JIT inlines → 1 direct call

Memory Layout:

Наслідування: Object Header (12) + A fields + B fields + C fields = один великий об'єкт
Композиція: Service (16 header + ref) + Delegate (16 header + fields) = два об'єкти + посилання

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

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

  • ✅ Плюси: Поліморфізм, code reuse, Template Method, framework extension
  • ❌ Мінуси: Fragile base class, API pollution, static binding, coupling

Композиція:

  • ✅ Плюси: Runtime flexibility, clean API, testability, loose coupling
  • ❌ Мінуси: Boilerplate (delegation), more objects, indirection overhead

Edge Cases

  1. Diamond Problem: Java забороняє множинне наслідування класів
    • Рішення: Композиція + кілька інтерфейсів (default methods)
  2. Sealed Classes (Java 17+): Обмеження кола нащадків
    public sealed class Expr permits Constant, Plus, Minus { }
    // Передбачувана ієрархія, pattern matching
    
  3. Framework Constraints: Spring/Hibernate іноді вимагають наслідування
    • Рішення: Ізолювати в одному шарі, використовувати композицію для бізнес-логіки

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

Метрика Наслідування Композиція
VTable depth 5+ рівнів → megamorphic N/A (direct calls)
Inlining Складніше при deep hierarchy Простіше з final
Memory Один великий об’єкт N об’єктів + посилання
Startup Швидше (менше об’єктів) Трохи повільніше
  • VTable lookup: ~1-3 ns для monomorphic, ~10-20 ns для megamorphic
  • Final delegate: JIT inlines → ~0.3 ns (прямий виклик)

Thread Safety

  • Наслідування: Один монитор на весь об’єкт → contention
  • Композиція: Різні lock’и для різних делегатів → finer-grained concurrency
class ConcurrentService {
    private final ReadDelegate read = new ReadDelegate();
    private final WriteDelegate write = new WriteDelegate();
    // Різні lock'и → паралельне виконання
}

Production Experience

Рефакторинг в e-commerce: OrderProcessor extends BaseService з 3000 рядків. Проблема: зміна логіки валідації ламала збереження. Рефакторинг: виділили OrderValidator, OrderRepository, NotificationService через композицію. Результат: кількість регресійних багів знизилася на 65%.

Monitoring

ArchUnit:

@ArchTest
static void no_deep_inheritance = classes()
    .should().haveLessThanNAncestors(3)
    .because("Deep inheritance is fragile");

SonarQube: Depth of Inheritance Tree > 5 → Code Smell

Best Practices for Highload

  • final delegates для inlining
  • Sealed classes для контрольованого наслідування
  • Interface + Composition замість deep hierarchies
  • Pure DI для критичних до продуктивності ділянок
  • Обирайте композицію за замовчуванням, але не фанатично. Наслідування — коли є справжнє IS-A, фреймворк вимагає, або спільні інфраструктурні поля.

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

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

  • Композиція переважніша: runtime-гнучкість, clean API, тестованість, loose coupling
  • Stack extends Vector — порушення інваріанта: Vector дозволяє insertElementAt(0) — LIFO зламано
  • MyHashSet extends HashSet — double count: addAll викликає add internally, лічильник подвоюється
  • VTable: 5+ рівнів → megamorphic (~10-20 ns), final delegate → JIT inlines (~0.3 ns)
  • Diamond Problem вирішується композицією + кілька інтерфейсів (default methods)
  • Sealed Classes (Java 17+) — обмеження кола нащадків

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

  • Чому Stack extends Vector поганий? — Клієнт Vector може обійти LIFO-інваріант стека через insertElementAt
  • Коли наслідування ДОПУСТИМЕ? — IS-A Relationship, Framework extension, Logical grouping (BaseEntity)
  • Що таке false sharing в контексті God Object? — Поля різних відповідальностей в одній cache line → CPU cores інвалідують кеш один одного
  • Thread Safety: композиція vs наслідування? — Композиція: різні lock’и для різних делегатів → finer-grained concurrency

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

  • “Наслідування від ArrayList — нормальний спосіб додати функції” (порушення інкапсуляції)
  • “Композиція завжди дає кращу продуктивність” (в hot path з switch по enum — наслідування може бути швидшим)
  • “BaseEntity — це порушення SRP” (прагматизм: всі сутності потребують id, головне — без бізнес-логіки)

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

  • [[10. Що таке композиція і наслідування]]
  • [[12. Що таке делегування в ООП]]
  • [[1. Що таке принцип Single Responsibility і як його застосовувати]]
  • [[18. Як рефакторити God Object (божественний об’єкт)]]