具有重叠行为的对象组合
Object composition with overlapping behaviors
如果没有示例,这将很难解释,所以我将使用 here 中的示例。
const canCast = (state) => ({
cast: (spell) => {
console.log(`${state.name} casts ${spell}!`);
state.mana--;
}
})
const mage = (name) => {
let state = {
name,
health: 100,
mana: 100
}
return Object.assign(state, canCast(state));
}
简单地说,我们有一个'mage'对象和一个'cast'行为。
现在假设我们想要一种新型法师,它可以在施放法术时吸取对手的生命值。一开始似乎很容易;只需创建一个新行为:
const canCastDrain = (state) => ({
cast: (spell) => {
console.log(`${state.name} casts ${spell}!`);
state.mana--;
target.health--;
state.health++;
}
})
但是,这迫使我们复制原始演员代码。你可以想象,对于更复杂的行为,这将是一个巨大的问题。如何避免这种情况?
如果我们使用经典继承,drain spell cast 可以扩展base cast 然后调用parent 方法。但随后我们陷入了继承问题。如果我们添加新的法术施法,就很难混合搭配它们。
这个答案是在某种 JS 伪代码 中给出的,因为我对我的面向对象的 JS 没有信心(我通常使用 TS)。
你的法师可能有一个 class 的角色基础,或者什么的。毕竟,每个人都有名字和健康。我省略了它,因为它与答案并不相关。问题是关于你的法术是如何构造的。
我非常有信心命令模式就是您所需要的。
法师有一些属性和两种施法方法。第一个决定法师是否可以施放该法术。你可以让法术有一个类别(或法术学校),或者你想检查权限。
pre-cast 和 post-cast 的方法虽然不是您问题的明确部分,但很可能会出现。也许咒语需要在调用其施法方法之前检查目标是否有效。
class Mage {
mana: number;
health: number;
name: string;
canCast(spell) {
// check if the mage knows the spell, or knows the school of magic, or whatever.
// can also check that the mage has the mana, though since this is common to every cast and doesn't vary, that can be moved into the actual cast method.
// return true/false
// this method can vary as needed
}
// should be the same for all mages.
// we call the spells pre-cast hooks before casting, for composite spells this ensures each sub-spell pre-hook is called before any of the spells
// are cast. This hook can be used to verify the spell *can* be cast (e.g. you have enough health)
cast(spell, target) {
if (spell.getCost() > mana) {
// not enough mana.
// this isn't part of canCast, because this applies to every mage, and canCast can vary.
// return or throw an error
}
console.log("Casting....");
if (!spell.preCast(this, target)) {
// maybe the target isn't valid for this spell?
// we do this before *any* spells are cast, so if one of them is not valid,
// there's nothing to "roll back" or "undo".
// either throw an error or return. either way, don't continue casting.
}
spell.cast(this, target);
spell.postCast(this, target);
this.deductMana(spell.getCost());
console.log("Done casting. Did we win?");
}
}
基本法术,没有任何功能,但充满了叫做 'love':
的东西
class Spell {
getName(): string;
getCost(): number;
preCast(target, caster, etc.) {}
cast(target, caster, etc.) {}
postCast(target, caster, etc.) {}
}
你的复合法术。一个 class 应该可以让你进行任意数量的组合,除非你需要非常专业的东西。例如,结合两个火焰法术可能会增加伤害,同时降低总法力消耗。那将需要一个特殊的复合咒语,SynergizingCompositeSpell
也许吧?
class CompositeSpell : Spell {
spells: Spell[];
getName {
// return the subspell names
}
getCost (
// return sum of subspell costs.
}
preCast(target, caster, etc.) {
// call each subspell's preCast
// if any return false, return false. otherwise, return true.
}
cast(target, caster, etc.) {
// call each spell's cast
}
postCast(target, caster, etc.) {
// call each spells post-cast
}
constructor(spell, spell, spell, etc). // stores the spells into the property
}
示例拼写:
class Drain : Spell {
getName() { return "Drain!"; }
getCost() { return 3; } // costs 3 mana
cast(target, caster, etc.) {
target.harm(1); // let the target deduct its hp
console.log(`The ${target.name} was drained for 3 hp and looks hoppin' mad.`)
}
}
这在使用中的样子,施放了一个耗尽生命值并使我的牙齿闪亮的咒语 chrome
var mage = ... // a mage
var target = ... // you, obviously
var spellToCast = new CompositeSpell(new Drain(), new ShinyAndChrome());
mage.cast(spellToCast, target);
CompositeSpell
构造函数可以检查它所给出的法术是否为 "compatible",无论这在您的游戏中可能意味着什么。法术也可以有一个 canBeCastWith(spell)
方法来验证兼容性。也许将 Drain
和 Heal
组合在一起毫无意义且不应该被允许?或者一个法术接受一个目标,但另一个不接受?
值得注意的是 preCast
/ cast
/ postCast
方法应该采用相同的参数,即使它们并不总是需要。您使用的是一种千篇一律的模式,因此您需要包括任何法术可能需要的一切。我想该列表仅限于:
- 施法者
- 目标
- 区域(效果法术的区域)
- 咒语选项(在龙与地下城中,施法者选择将某人变形为什么)
我想指出的一件事是,不要直接使用 addition/subtraction 来处理你的生命值或法力值(例如 state.mana--
),而是使用函数调用(例如 state.useMana(1)
。这让您的选择在未来的发展中保持开放。
例如,如果您的法师具有在 his/her 生命值减少时 触发 的能力怎么办?该咒语不知道它应该触发任何东西。那要看性格了。这也让您可以覆盖该方法,这是您无法使用简单的 addition/subtraction.
希望这个回答对您有所帮助。
如果没有示例,这将很难解释,所以我将使用 here 中的示例。
const canCast = (state) => ({
cast: (spell) => {
console.log(`${state.name} casts ${spell}!`);
state.mana--;
}
})
const mage = (name) => {
let state = {
name,
health: 100,
mana: 100
}
return Object.assign(state, canCast(state));
}
简单地说,我们有一个'mage'对象和一个'cast'行为。
现在假设我们想要一种新型法师,它可以在施放法术时吸取对手的生命值。一开始似乎很容易;只需创建一个新行为:
const canCastDrain = (state) => ({
cast: (spell) => {
console.log(`${state.name} casts ${spell}!`);
state.mana--;
target.health--;
state.health++;
}
})
但是,这迫使我们复制原始演员代码。你可以想象,对于更复杂的行为,这将是一个巨大的问题。如何避免这种情况?
如果我们使用经典继承,drain spell cast 可以扩展base cast 然后调用parent 方法。但随后我们陷入了继承问题。如果我们添加新的法术施法,就很难混合搭配它们。
这个答案是在某种 JS 伪代码 中给出的,因为我对我的面向对象的 JS 没有信心(我通常使用 TS)。
你的法师可能有一个 class 的角色基础,或者什么的。毕竟,每个人都有名字和健康。我省略了它,因为它与答案并不相关。问题是关于你的法术是如何构造的。
我非常有信心命令模式就是您所需要的。
法师有一些属性和两种施法方法。第一个决定法师是否可以施放该法术。你可以让法术有一个类别(或法术学校),或者你想检查权限。
pre-cast 和 post-cast 的方法虽然不是您问题的明确部分,但很可能会出现。也许咒语需要在调用其施法方法之前检查目标是否有效。
class Mage {
mana: number;
health: number;
name: string;
canCast(spell) {
// check if the mage knows the spell, or knows the school of magic, or whatever.
// can also check that the mage has the mana, though since this is common to every cast and doesn't vary, that can be moved into the actual cast method.
// return true/false
// this method can vary as needed
}
// should be the same for all mages.
// we call the spells pre-cast hooks before casting, for composite spells this ensures each sub-spell pre-hook is called before any of the spells
// are cast. This hook can be used to verify the spell *can* be cast (e.g. you have enough health)
cast(spell, target) {
if (spell.getCost() > mana) {
// not enough mana.
// this isn't part of canCast, because this applies to every mage, and canCast can vary.
// return or throw an error
}
console.log("Casting....");
if (!spell.preCast(this, target)) {
// maybe the target isn't valid for this spell?
// we do this before *any* spells are cast, so if one of them is not valid,
// there's nothing to "roll back" or "undo".
// either throw an error or return. either way, don't continue casting.
}
spell.cast(this, target);
spell.postCast(this, target);
this.deductMana(spell.getCost());
console.log("Done casting. Did we win?");
}
}
基本法术,没有任何功能,但充满了叫做 'love':
的东西class Spell {
getName(): string;
getCost(): number;
preCast(target, caster, etc.) {}
cast(target, caster, etc.) {}
postCast(target, caster, etc.) {}
}
你的复合法术。一个 class 应该可以让你进行任意数量的组合,除非你需要非常专业的东西。例如,结合两个火焰法术可能会增加伤害,同时降低总法力消耗。那将需要一个特殊的复合咒语,SynergizingCompositeSpell
也许吧?
class CompositeSpell : Spell {
spells: Spell[];
getName {
// return the subspell names
}
getCost (
// return sum of subspell costs.
}
preCast(target, caster, etc.) {
// call each subspell's preCast
// if any return false, return false. otherwise, return true.
}
cast(target, caster, etc.) {
// call each spell's cast
}
postCast(target, caster, etc.) {
// call each spells post-cast
}
constructor(spell, spell, spell, etc). // stores the spells into the property
}
示例拼写:
class Drain : Spell {
getName() { return "Drain!"; }
getCost() { return 3; } // costs 3 mana
cast(target, caster, etc.) {
target.harm(1); // let the target deduct its hp
console.log(`The ${target.name} was drained for 3 hp and looks hoppin' mad.`)
}
}
这在使用中的样子,施放了一个耗尽生命值并使我的牙齿闪亮的咒语 chrome
var mage = ... // a mage
var target = ... // you, obviously
var spellToCast = new CompositeSpell(new Drain(), new ShinyAndChrome());
mage.cast(spellToCast, target);
CompositeSpell
构造函数可以检查它所给出的法术是否为 "compatible",无论这在您的游戏中可能意味着什么。法术也可以有一个 canBeCastWith(spell)
方法来验证兼容性。也许将 Drain
和 Heal
组合在一起毫无意义且不应该被允许?或者一个法术接受一个目标,但另一个不接受?
值得注意的是 preCast
/ cast
/ postCast
方法应该采用相同的参数,即使它们并不总是需要。您使用的是一种千篇一律的模式,因此您需要包括任何法术可能需要的一切。我想该列表仅限于:
- 施法者
- 目标
- 区域(效果法术的区域)
- 咒语选项(在龙与地下城中,施法者选择将某人变形为什么)
我想指出的一件事是,不要直接使用 addition/subtraction 来处理你的生命值或法力值(例如 state.mana--
),而是使用函数调用(例如 state.useMana(1)
。这让您的选择在未来的发展中保持开放。
例如,如果您的法师具有在 his/her 生命值减少时 触发 的能力怎么办?该咒语不知道它应该触发任何东西。那要看性格了。这也让您可以覆盖该方法,这是您无法使用简单的 addition/subtraction.
希望这个回答对您有所帮助。