Наследование и композиция: использование ролевой игры на JavaScript в качестве примера

Наследование и композиция: использование ролевой игры на 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 идеально подходит для этого.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE