En este artículo, veremos de qué se tratan estos problemas y cómo podemos resolverlos usando la composición.
el problema con los lenguajes orientados a objetos es que tienen todo este entorno implícito que llevan consigo. Querías una banana pero lo que obtuviste fue un gorila sosteniendo la banana y toda la jungla. - Joe Armstrong, creador de Erlang
Considere el proceso de crear una jerarquía de personajes de juegos de rol. Inicialmente, se requieren dos tipos de personajes: Guerrero y Mago, cada uno de los cuales tiene una cierta cantidad de salud y un nombre. Estas propiedades son públicas y se pueden mover a la clase de carácter principal.
class Character { constructor(name) { this.name = name; this.health = 100; } }
Un guerrero puede atacar, gastando su energía:
class Warrior extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina--; } }
Y un mago puede lanzar hechizos que gastan cierta cantidad de maná:
class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana--; } }
Ahora, presentemos una nueva clase, Paladín . Un paladín puede luchar y lanzar hechizos. ¿Cómo podemos solucionar esto? Aquí hay un par de soluciones que comparten la misma falta de elegancia:
fight()
y cast()
desde cero. En este caso, se viola el principio DRY porque cada uno de los métodos se duplicará en el momento de la creación y necesitará una sincronización constante con los métodos de las clases Mage y Fighter para realizar un seguimiento de los cambios.
fight()
y cast()
se pueden implementar en el nivel de clase de personaje para que los tres tipos de personajes los tengan. Esta es una solución un poco mejor, pero en este caso, el desarrollador debe anular el método fight()
para el mago y el método cast() para el guerrero, reemplazándolos con métodos vacíos o consolando un error.Estos problemas se pueden resolver con un enfoque funcional utilizando la composición. Basta partir no de sus tipos, sino de sus funciones. Básicamente, tenemos dos características clave que determinan las habilidades de los personajes: la habilidad para luchar y la habilidad para lanzar hechizos.
Estas características se pueden configurar mediante funciones de fábrica que amplían el estado que define el carácter:
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--; } })
Así, un personaje se define por un conjunto de estas habilidades y propiedades iniciales, tanto generales (nombre y salud) como privadas (resistencia y maná):
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)); }
Con la composición, puede evitar problemas de herencia usando la composición, y javascript es el lenguaje perfecto para hacerlo.