Inheritance Problems

In this article, we will look at what these problems are about and how we can solve them using composition.

the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong, creator of Erlang

Role-Playing Game Inheritance

Consider the process of creating a hierarchy of role-playing game characters. Initially, two types of characters are required - Warrior and Mage, each of which has a certain amount of health and a name. These properties are public and can be moved to the parent Character class.

class Character { 
   constructor(name) { 
      this.name = name; 
      this.health = 100;
   } 
}

A warrior can strike, spending his stamina:

class Warrior extends Character { 
   constructor(name) { 
      super(name); 
      this.stamina = 100; 
   } 
   fight() { 
      console.log(`${this.name} takes a mighty swing!`); 
      this.stamina--; 
   } 
}

And a mage can cast spells that spend some amount of mana:

class Mage extends Character {
   constructor(name) { 
      super(name); 
      this.mana = 100; 
   } 
   cast() { 
      console.log(`${this.name} casts a fireball!`); 
   this.mana--; 
   } 
}

Paladin Class Problem

Now, let’s introduce a new class, Paladin. A Paladin can both fight and cast spells. How can we solve this? Here are a couple of solutions that share the same lack of elegance:

Composition

These problems can be solved with a functional approach using composition. It is enough to start not from their types, but from their functions. Basically, we have two key features that determine the abilities of the characters - the ability to fight and the ability to cast spells.

These features can be set using factory functions that extend the state that defines the character:

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--; 
   } 
})

Thus, a character is defined by a set of these abilities and initial properties, both general (name and health) and private (stamina and mana):

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)); 
}

Conclusion

With composition, you can avoid inheritance problems, and javascript is the perfect language to do so.