Jan 01, 1970
在这篇文章中,我们将看看这些问题是什么以及我们如何使用组合来解决它们。
面向对象语言的问题在于它们拥有随身携带的所有这些隐式环境。你想要一根香蕉,但你得到的是一只拿着香蕉和整个丛林的大猩猩。 - Joe Armstrong,Erlang 的创造者
考虑创建角色扮演游戏角色层次结构的过程。最初,需要两种类型的角色——战士和法师,每个角色都有一定的生命值和名字。这些属性是公共的,可以移至父 Character 类。
class Character { constructor(name) { this.name = name; this.health = 100; } }
战士可以攻击,消耗他的耐力:
class Warrior extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina--; } }
法师可以施放消耗一定法力的法术:
class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana--; } }
现在,让我们介绍一个新职业,圣骑士。圣骑士既可以战斗也可以施法。我们如何解决这个问题?以下是一些同样缺乏优雅的解决方案:
fight()
和cast()
方法。在这种情况下,违反了DRY原则,因为每个方法在创建时都会被复制,并且需要与 Mage 和 Fighter 类的方法不断同步以跟踪变化。
fight()
和cast()
方法可以在Character类级别实现,以便所有三种角色类型都具有它们。这是一个稍微好一点的解决方案,但在这种情况下,开发人员必须覆盖法师的fight()
方法和战士的 cast() 方法,将它们替换为空方法或解决错误。这些问题可以通过使用组合的函数式方法来解决。不从它们的类型开始,而是从它们的功能开始就足够了。基本上,我们有两个决定角色能力的关键特征——战斗能力和施法能力。
可以使用扩展定义角色状态的工厂函数来设置这些功能:
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--; } })
因此,角色由一组这些能力和初始属性定义,包括一般(名称和健康)和私有(耐力和法力):
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)); }
通过组合,您可以使用组合来避免继承问题,而 javascript 是这样做的完美语言。