Вопрос 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 — нарушение инвариантa: 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 (божественный объект)]]