Neste artigo, veremos do que se tratam esses problemas e como podemos resolvê-los usando a composição.
o problema com as linguagens orientadas a objetos é que elas têm todo esse ambiente implícito que carregam consigo. Você queria uma banana, mas o que conseguiu foi um gorila segurando a banana e toda a selva. - Joe Armstrong, criador de Erlang
Considere o processo de criação de uma hierarquia de personagens de RPG. Inicialmente, são necessários dois tipos de personagens - Guerreiro e Mago, cada um com uma certa quantidade de saúde e um nome. Essas propriedades são públicas e podem ser movidas para a classe Character pai.
class Character { constructor(name) { this.name = name; this.health = 100; } }
Um guerreiro pode atacar, gastando sua resistência:
class Warrior extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina--; } }
E um mago pode lançar feitiços que gastam uma certa quantidade de mana:
class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana--; } }
Agora, vamos apresentar uma nova classe, Paladino . Um Paladino pode lutar e lançar feitiços. Como podemos resolver isso? Aqui estão algumas soluções que compartilham a mesma falta de elegância:
fight()
e cast()
a partir do zero. Nesse caso, o princípio DRY é violado porque cada um dos métodos será duplicado na criação e precisará de sincronização constante com os métodos das classes Mage e Fighter para rastrear as alterações.
fight()
e cast()
podem ser implementados no nível da classe Character para que todos os três tipos de personagem os tenham. Esta é uma solução um pouco melhor, mas neste caso, o desenvolvedor deve substituir o método fight()
para o mago e o método cast() para o guerreiro, substituindo-os por métodos vazios ou consertando um erro.Esses problemas podem ser resolvidos com uma abordagem funcional usando composição. Basta partir não de seus tipos, mas de suas funções. Basicamente, temos duas características principais que determinam as habilidades dos personagens - a habilidade de lutar e a habilidade de lançar feitiços.
Esses recursos podem ser definidos usando funções de fábrica que estendem o estado que define o caractere:
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--; } })
Assim, um personagem é definido por um conjunto dessas habilidades e propriedades iniciais, tanto gerais (nome e saúde) quanto particulares (resistência e 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)); }
Com a composição, você pode evitar problemas de herança usando composição, e o javascript é a linguagem perfeita para fazer isso.