Наследование vs композиция
Наследование строит иерархию «является» (IS-A), а композиция — «содержит» (HAS-A); в JavaScript рекомендуется предпочитать композицию для гибкой и слабосвязанной архитектуры.
Зачем нужно
Выбор между наследованием и композицией определяет гибкость и поддерживаемость кода. Глубокие иерархии наследования (3+ уровней) становятся хрупкими: изменение базового класса ломает все потомки. Композиция позволяет собирать объекты из независимых частей и менять поведение без риска поломки цепочки.
Где используется
- Проектирование бизнес-логики: сервисы, репозитории
- React: HOC vs hooks (hooks — пример победы композиции над наследованием)
- Entity-Component-System паттерн
- Фабрики и стратегии
Наследование (IS-A)
class Animal {
constructor(name) { this.name = name; }
eat { return `${this.name} ест`; }
}
class Dog extends Animal {
bark { return 'Гав!'; }
}
class GuideDog extends Dog {
guide { return `${this.name} ведёт хозяина`; }
}
const buddy = new GuideDog('Бадди');
console.log(buddy.eat); // Бадди ест
console.log(buddy.bark); // Гав!
console.log(buddy.guide); // Бадди ведёт хозяина
Проблема: если нужна RobotDog без метода eat, иерархия ломается.
Проблема хрупкого базового класса
class Base {
method {
this.helper; // child может переопределить helper
}
helper { return 'base'; }
}
class Child extends Base {
helper { return 'child'; } // меняет поведение Base.method!
}
// Изменение Base.method ломает все дочерние классы
Композиция (HAS-A)
// Независимые поведения
const canEat = {
eat { return `${this.name} ест`; }
};
const canBark = {
bark { return 'Гав!'; }
};
const canGuide = {
guide { return `${this.name} ведёт хозяина`; }
};
// Фабрика с композицией
function createGuideDog(name) {
return Object.assign(
{ name },
canEat,
canBark,
canGuide
);
}
function createRobotDog(name) {
return Object.assign(
{ name },
canBark, // нет canEat — роботу не нужно есть
canGuide
);
}
const buddy = createGuideDog('Бадди');
const robo = createRobotDog('Р2Д2');
console.log(buddy.eat); // Бадди ест
console.log(robo.bark); // Гав!
// robo.eat — нет такого метода, и это правильно
Функциональная композиция
// Поведения как функции-замыкания
const withLogging = (obj) => ({
...obj,
log(msg) { console.log(`[${obj.name}] ${msg}`); }
});
const withValidation = (obj) => ({
...obj,
validate(data) { return data !== null; }
});
const createService = (name) =>
withLogging(withValidation({ name }));
const svc = createService('UserService');
svc.log('Запуск'); // [UserService] Запуск
console.log(svc.validate(42)); // true
Когда использовать наследование
Наследование уместно, когда:
- Отношение действительно IS-A (квадрат IS-A форма)
- Иерархия неглубокая (1-2 уровня)
- Есть общая реализация, которую нужно переопределять (template method)
// Уместное наследование: компоненты UI
class Component {
render { throw new Error('Implement render'); }
mount(container) { container.innerHTML = this.render; }
}
class Button extends Component {
constructor(label) { super; this.label = label; }
render { return `<button>${this.label}</button>`; }
}
Частые ошибки
1. Глубокие иерархии (антипаттерн)
// Плохо: Animal → Mammal → Pet → Dog → GuideDog → ...
// Каждый уровень добавляет хрупкость
// Лучше: плоская структура + миксины/композиция
2. Наследование ради переиспользования кода
// Плохо: наследуем только чтобы получить метод
class UserService extends DatabaseService { // не IS-A
// использует только db.query
}
// Лучше: инжектируем зависимость
class UserService {
constructor(db) { this.db = db; }
getUser(id) { return this.db.query(id); }
}
Три типа композиции
автор (tOIcBrzezK0) выделяет три разных типа отношений, которые часто называют «композиция»:
- Ассоциация — голая ссылка, инициализируется
null, может подменяться на лету - Агрегация — готовый объект внедряется через конструктор (DI)
- Композиция — связанный объект создаётся внутри конструктора владельца
См. подробно Ассоциация, агрегация, композиция.
Тезисы автора о вреде наследования
- «Длинные цепочки наследования запутывают код и добавляют ненужной жёсткости» (tOIcBrzezK0).
- «Наследование замедляет исполнение, но не существенно. Замедляет чтение и тем более модификацию кода» (9d5TG1VsLeU).
- 5-7 уровней наследования — это уже антипаттерн (см. Антипаттерны ООП).
Когда наследование оправдано
- Полная семантика IS-A:
Squareдействительно прямоугольник - Базовый класс предоставляет инфраструктуру (
EventEmitter,HTMLElement) - Цепочка короткая (1-2 уровня)
- Поведение только расширяется, не изменяется (LSP)
Связанные темы
- Миксины (Mixins)
- Ассоциация, агрегация, композиция
- Полиморфизм
- Антипаттерны ООП
- Composition Pattern
- Dependency Inversion Principle
- _MOC ООП
- _MOC Паттерны
Источники
- Timur · Object Association, Aggregation, and Composition (2019-10-31)
- Timur · ООП: наследование и полиморфизм в JavaScript (2020-03-03)
- Timur · OOP Anti-Patterns Part 1 (2019-11-21)
- Timur · Have Objects Failed? OOP critique (2019-05-23)
- JavaScript.info — Классы