В каких случаях лучше использовать композицию вместо наследования
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— нарушение инвариантa: 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 (божественный объект)]]