У яких випадках краще використовувати композицію замість наслідування
VTable — таблиця вказівників на методи. Monomorphic = JIT знає точний тип → inlining (~1 ns). Megamorphic = 3+ типи → пошук по таблиці (~10-20 ns).
🟢 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
- Diamond Problem: Java забороняє множинне наслідування класів
- Рішення: Композиція + кілька інтерфейсів (default methods)
- Sealed Classes (Java 17+): Обмеження кола нащадків
public sealed class Expr permits Constant, Plus, Minus { } // Передбачувана ієрархія, pattern matching - 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викликаєaddinternally, лічильник подвоюється- 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 (божественний об’єкт)]]