Наследование и композиция: использование ролевой игры на JavaScript в качестве примера
22 декабря 2022 г.Проблемы с наследованием
- Дублирование кода у детей
- Излишняя сложность иерархии наследования
- Изменение поведения родителя может привести к ошибкам в дочерних элементах.
В этой статье мы рассмотрим, в чем заключаются эти проблемы и как мы можем решить их с помощью композиции.
<цитата>проблема с объектно-ориентированными языками заключается в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но получили гориллу, держащую банан и все джунгли. – Джо Армстронг, создатель Erlang
Наследование ролевых игр
Рассмотрите процесс создания иерархии персонажей ролевой игры. Изначально требуется два типа персонажей - Воин и Маг, каждый из которых имеет определенное количество здоровья и имя. Эти свойства являются общедоступными и могут быть перемещены в родительский класс символов.
class Character {
constructor(name) {
this.name = name;
this.health = 100;
}
}
Воин может наносить удары, тратя свою выносливость:
class Warrior extends Character {
constructor(name) {
super(name);
this.stamina = 100;
}
fight() {
console.log(`${this.name} takes a mighty swing!`);
this.stamina--;
}
}
И маг может читать заклинания, которые тратят некоторое количество маны:
class Mage extends Character {
constructor(name) {
super(name);
this.mana = 100;
}
cast() {
console.log(`${this.name} casts a fireball!`);
this.mana--;
}
}
Проблема класса Паладин
Теперь давайте представим новый класс, Паладин. Паладин может сражаться и использовать заклинания. Как мы можем решить эту проблему? Вот несколько решений, которые не лишены элегантности:
* Мы можем сделать Paladin потомком Character и реализовать методы fight()
и cast()
в это с нуля. В этом случае нарушается принцип DRY, поскольку каждый из методов будет дублироваться при создании и будет нуждаться в постоянной синхронизации с методами классов Mage и Fighter для отслеживания изменений.
* Методы fight()
и cast()
могут быть реализованы на уровне класса Character, чтобы они были доступны для всех трех типов персонажей. Это немного лучшее решение, но в этом случае разработчик должен переопределить метод fight()
для мага и метод cast() для воина, заменив их пустыми методами или утешив ошибку. .
Композиция
Эти проблемы можно решить с помощью функционального подхода с использованием композиции. Достаточно исходить не из их видов, а из их функций. По сути, у нас есть две ключевые особенности, определяющие способности персонажей — способность сражаться и способность кастовать заклинания.
Эти функции можно настроить с помощью фабричных функций, которые расширяют состояние, определяющее символ:
const canCast = (state) => ({
cast: (spell) => {
console.log(`${state.name} casts ${spell}!`);
state.mana--;
}
})
const canFight = (state) => ({
fight: () => {
console.log(`${state.name} slashes at the foe!`);
state.stamina--;
}
})
Таким образом, персонаж определяется набором этих способностей и начальных свойств, как общих (имя и здоровье), так и частных (выносливость и мана):
const fighter = (name) => {
let state = { name, health: 100, stamina: 100 }
return Object.assign(state, canFight(state));
}
const mage = (name) => {
let state = { name, health: 100, mana: 100 }
return Object.assign(state, canCast(state));
}
const paladin = (name) => {
let state = { name, health: 100, mana: 100, stamina: 100 }
return Object.assign(state, canCast(state), canFight(state));
}
Заключение
С композицией вы можете избежать проблем с наследованием, и javascript идеально подходит для этого.
Оригинал