Dans cet article, nous verrons en quoi consistent ces problèmes et comment nous pouvons les résoudre en utilisant la composition.
le problème avec les langages orientés objet est qu'ils ont tout cet environnement implicite qu'ils transportent avec eux. Tu voulais une banane mais ce que tu as eu c'est un gorille tenant la banane et toute la jungle. - Joe Armstrong, créateur d'Erlang
Considérez le processus de création d'une hiérarchie de personnages de jeux de rôle. Initialement, deux types de personnages sont requis - Guerrier et Mage, chacun ayant une certaine santé et un nom. Ces propriétés sont publiques et peuvent être déplacées vers la classe parent Character.
class Character { constructor(name) { this.name = name; this.health = 100; } }
Un guerrier peut frapper en dépensant son endurance :
class Warrior extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina--; } }
Et un mage peut lancer des sorts qui dépensent une certaine quantité de mana :
class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana--; } }
Introduisons maintenant une nouvelle classe, Paladin . Un paladin peut à la fois combattre et lancer des sorts. comment pouvons nous résoudre ceci? Voici quelques solutions qui partagent le même manque d'élégance :
fight()
et cast()
à partir de rien. Dans ce cas, le principe DRY est violé car chacune des méthodes sera dupliquée à la création et nécessitera une synchronisation constante avec les méthodes des classes Mage et Fighter pour suivre les changements.
fight()
et cast()
peuvent être implémentées au niveau de la classe Character afin que les trois types de caractères les aient. C'est une solution légèrement meilleure, mais dans ce cas, le développeur doit remplacer la méthode fight()
pour le mage et la méthode cast() pour le guerrier, en les remplaçant par des méthodes vides ou en consolant une erreur.Ces problèmes peuvent être résolus avec une approche fonctionnelle utilisant la composition. Il suffit de partir non de leurs types, mais de leurs fonctions. Fondamentalement, nous avons deux caractéristiques clés qui déterminent les capacités des personnages : la capacité de se battre et la capacité de lancer des sorts.
Ces fonctionnalités peuvent être définies à l'aide de fonctions d'usine qui étendent l'état qui définit le caractère :
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--; } })
Ainsi, un personnage est défini par un ensemble de ces capacités et propriétés initiales, à la fois générales (nom et santé) et privées (endurance et 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)); }
Avec la composition, vous pouvez éviter les problèmes d'héritage en utilisant la composition, et javascript est le langage parfait pour le faire.