
Композиция вместо наследования — это принцип, согласно которому классы должны достигать полиморфного поведения и повторного использования кода путем их композиции, а не наследования от базы.
Наследование
Чтобы лучше понять, почему мы можем предпочесть композицию наследованию, давайте сначала рассмотрим наследование в Javascript, а именно в ES6. Ключевое слово extends используется в объявлениях или выражениях для создания класса, который является дочерним по отношению к другому.
class Plant{ constructor(name){ this.name = name } water(){ console.log("Water the " + this.name) } repot(){ console.log( "Repot the " + this.name) }harvest(){ console.log("Harvest the " + this.name) } }class Vegetable extends Plant { constructor(name, size, health){ super(name) this.health = health; } }class Flower extends Plant { constructor(name, size, health){ super(name) this.health = health; } }class Fruit extends Plant { constructor(name, size, health){ super(name) this.health = health; } }
Мы видим потенциальную проблему, которая начинает формироваться при использовании модели наследования.
Метод water является общим для экземпляров Flower, Vegetable и Fruit, что полезно, поскольку все они нуждаются в поливе (watered), но нет необходимости, чтобы экземпляр Flower имел доступ к методу harvest (сбору урожая), а так как мои овощи высажены в землю, поэтому нет причин, чтобы они имели доступ к методу repot (пересадка).
Ассоциации должны выглядеть следующим образом:
Фрукты поливаются, пересаживаются, собираются.
Цветы поливаются, пересаживаются в горшок
Овощи поливаются, собираются
Хорошо, а что если я сделаю что-то вроде следующего
class Plant{ constructor(name){ this.name = name } water(){ console.log("Water the " + this.name) } }class Vegetable extends Plant { constructor(name, size, health){ super(name) this.health = health; } harvest(){ console.log("Harvest the " + this.name) } }class Flower extends Plant { constructor(name, size, health){ super(name) this.health = health; } repot(){ console.log( "Repot the " + this.name) }}class Fruit extends Plant { constructor(name, size, health){ super(name) this.health = health; } repot(){ console.log( "Repot the " + this.name) } harvest(){ console.log("Harvest the " + this.name) } }
Это немного лучше, но теперь мы создаем дублирующие методы на разных экземплярах, которые делают одно и то же, что не соответствует принципам DRY (Don’t Repeat Yourself). Это проблема, которая может быть порождена паттерном наследования.
Проблема объектно-ориентированных языков в том, что они имеют всю эту неявную среду, которую переносят с собой. Вы хотели банан, а получили гориллу, которая держит банан и целые джунгли впридачу. - Джо Армстронг. Создатель Erlang.
Наследование по своей природе является сильно связанным по сравнению с композицией. Модель наследования вынуждает нас предсказывать будущее и строить таксономию типов. Поэтому, если мы не можем прогнозировать будущее, то неизбежно получим несколько ошибок.
Композиция
Здесь нам может помочь композиционный паттерн.
const harvest = () => { console.log("Harvesting") }const water = () => { console.log("Watering) }const repot = () => { console.log( "Repotting") }const Flower = (name) => { return Object.assign( {name}, water(), repot() ) }const Vegatable = (name) => { return Object.assign( {name}, water(), harvest() ) }const Fruit = (name) => { return Object.assign( {name}, water(), repot(), harvest() ) }const daffodil = Plant(); daffodil.harvest() // undefined const banana = Fruit(); banana.harvest() // Harvesting
Отдавая предпочтение композиции перед наследованием и рассуждая с точки зрения того, что вещи делают, а не чем они являются, можно увидеть, что мы освободились от жестко связанной структуры наследования.
Нам больше не нужно предсказывать будущее, потому что дополнительные методы могут быть легко добавлены и включены в отдельные классы.
Можно заметить, что мы больше не полагаемся на прототипное наследование, а вместо этого используем функциональное инстанцирование для создания объекта. После инстанцирования переменная утрачивает связь с общими методами. Таким образом, никакие изменения не будут переданы экземплярам, инстанцированным до этого.
Если это является проблемой, мы все еще можем использовать прототипное наследование и композицию вместе, чтобы добавить новые свойства к прототипам после их создания и таким образом сделать их доступными для всех объектов, которые ему делегируются.
Выражение стрелочной функции больше не может быть использовано, поскольку оно не имеет встроенного метода конструктора.
function Vegatable(name) { this.name = name return Object.assign( this, water(), harvest() ) }const Carrot = new Vegatable('Carrot')
В заключение
Композиция удобна, когда мы описываем отношения "имеет", в то время как наследование полезно при описании отношений "является".
И то, и другое способствует повторному использованию кода. В отдельных случаях, в зависимости от требований и решения, применение наследования может иметь смысл.
Но подавляющее большинство решений заставят вас думать не только о текущих требованиях, но и о том, что понадобится в будущем, и в этом случае чаще всего побеждает композиция.
. . .
Вот и все. Я надеюсь, что вы нашли это полезным и благодарю за чтение. Если эта статья понравилась и она оказалась интересной, вам также могут пригодиться некоторые из других идей, которые мы создали на !!!nerdy. Новые идеи появляются каждый месяц.
Материал подготовлен в рамках курса «JavaScript Developer. Professional». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
