Inheritance Problems
- Duplication of code in children
- Excessive complexity in the inheritance hierarchy
- Changing the behavior of the parent can lead to errors in the children
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:
We can make Paladin a descendant of Character and implement both the fight() and cast() methods in it from scratch. In this case, the DRY principle is violated because each of the methods will be duplicated upon creation and will need constant synchronization with the methods of the Mage and Fighter classes to track changes.
The fight() and cast() methods can be implemented at the Character class level so that all three character types have them. This is a slightly better solution, but in this case, the developer must override the fight() method for the mage and the cast() method for the warrior, replacing them with empty methods or consoling an error.
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.
Top comments (0)