Почему часто советуют предпочитать композицию наследованию?
Смежные принципы проектирования
Наследование — самая сильная связь между классами: наследник зависит от деталей реализации родителя. Композиция собирает поведение из независимых частей, которые меняются и комбинируются даже во время выполнения.
Проблемы наследования
- Хрупкий базовый класс: безобидная правка родителя ломает наследников. Пример Блоха из «Effective Java»: счётчик в наследнике
HashSetпереопределяетaddиaddAll, а родительскийaddAllсам вызываетadd— элементы считаются дважды. - Комбинаторный взрыв:
LoggedCachedRepository,CachedLoggedRepository— иерархия растёт как произведение осей вариативности. - Одиночное наследование в TS и Java: поведение из двух веток не взять.
- Иерархия фиксируется при компиляции.
Композиция
/** Контракты: Repo — { get(id): Promise<item | undefined> }, Logger — { log(msg) } */
class LoggedRepo {
#inner;
#logger;
constructor(inner, logger) {
this.#inner = inner;
this.#logger = logger;
}
async get(id) {
this.#logger.log(`get ${id}`);
return this.#inner.get(id);
}
}Декоратор оборачивает любой Repo: кэширование и логирование комбинируются в любом порядке без новых классов.
Когда наследование уместно
- Настоящее «является» с соблюдением LSP.
- Этого требует платформа:
extends Error,HTMLElementв Web Components. - В React документация прямо советует композицию: для иерархий компонентов в Facebook не нашли случаев применения.
Коротко для собеседования
Наследник привязан к деталям родителя, поэтому иерархии хрупки и разрастаются комбинаторно. Композиция и декораторы собирают поведение из независимых частей в любом сочетании, а наследование оставляют для настоящего «является» с соблюдением LSP или для требований платформы.