【问题标题】:Object composition with overlapping behaviors具有重叠行为的对象组合
【发布时间】:2018-11-23 10:55:06
【问题描述】:

如果没有示例,这将很难解释,所以我将使用来自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));
}

简单地说,我们有一个“法师”对象和一个“施法”行为。

现在说我们想要一种新型的法师,可以在施法时消耗对手的生命值。一开始似乎很容易;只需创建一个新行为:

const canCastDrain = (state) => ({
    cast: (spell) => {
        console.log(`${state.name} casts ${spell}!`);
        state.mana--;
        target.health--;
        state.health++;
    }
})

但是,这迫使我们复制原始的演员表代码。你可以想象,对于更复杂的行为,这将是一个巨大的问题。如何避免这种情况?

如果我们使用经典继承,drain 法术施法可以扩展基础施法,然后调用父方法。但随后我们陷入了继承问题。如果我们添加新的施法,将很难混合和匹配它们。

【问题讨论】:

  • 我认为您需要使用Command 模式。有一个方法接受定义要施放的咒语的参数。然后调用那个法术的方法。这样你的法师就有一个方法来施法,但每个法术都有自己的功能。
  • @Amy 我认为这不能解决我正在谈论的问题。您仍然需要在所有拼写实现中复制代码。
  • 需要复制哪些代码?你知道,法术可以从基类法术继承。
  • 每个法术(命令)对象都有定义其法力消耗、法术类别和执行法术的方法的属性。单个拼写命令可以有额外的属性。每个法术都可以定义施法前和施法后的钩子。你的法师的施法方法检查类别,看他们是否可以施法对象,从法师的法力中扣除法力消耗,将法术执行记录到控制台,然后调用法术执行方法来执行其自定义行为。
  • 好的,我会写一个伪代码答案。如果您希望将其用作使用 actual JS 编写和接受自己的答案的跳板,我不反对。

标签: javascript object composition


【解决方案1】:

这个答案是在某种 JS 伪代码中给出的,因为我对我的面向对象的 JS 没有信心(我通常使用 TS)。

你的法师可能有一个 Character 的基类,或者什么。毕竟,每个人都有名字和健康。我省略了这一点,因为它与答案并不相关。问题在于你的法术是如何构成的。

我非常有信心命令模式是您所需要的。

Mage 有一些属性和两种施法方法。第一个决定法师是否可以施放该法术。你可以让法术有一个类别(或法术学校),或者你想检查权限。

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?");
    }
}

基础法术,没有任何功能,但充满了那种叫做“爱”的东西:

class Spell {
    getName(): string;
    getCost(): number;

    preCast(target, caster, etc.) {}
    cast(target, caster, etc.) {}
    postCast(target, caster, etc.) {}       
}

你的复合法术。一个类应该让你做任意数量的组合,除非你需要一些非常专业的东西。例如,结合两个火焰法术可能会放大伤害,同时降低总法力消耗。这需要一个特殊的复合咒语,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.`)
    }
}

使用时的样子,施法会消耗生命值,让我的牙齿有光泽和铬

var mage = ... // a mage
var target = ... // you, obviously
var spellToCast = new CompositeSpell(new Drain(), new ShinyAndChrome());
mage.cast(spellToCast, target);

CompositeSpell 构造函数可以检查它给出的咒语是否“兼容”,无论这在您的游戏中可能意味着什么。 Spells 也可以有一个canBeCastWith(spell) 方法来验证兼容性。也许将DrainHeal 组合在一起没有意义并且不应该被允许?或者一个咒语接受一个目标,而另一个不接受?

值得注意的是,preCast / cast / postCast 方法应该采用相同的参数,即使它们并不总是需要。您正在使用一种千篇一律的模式,因此您需要包含任何咒语可能需要的所有内容。我想该列表仅限于:

  • 施法者
  • 目标
  • 区域(用于法术效果区域)
  • 法术选项(在龙与地下城中,施法者选择将某人变形为什么)

我想指出的一件事是,不要直接对你的生命值或法力使用加法/减法(例如state.mana--),而是使用函数调用(例如state.useMana(1)。这让你的选择保持开放未来的发展。

例如,如果你的法师有一个技能会在他/她的生命值降低时触发?这个咒语不知道它应该触发什么。这就看性格了。这也可以让您覆盖该方法,这是简单的加法/减法无法做到的。

我希望这个答案会有所帮助。

【讨论】:

  • 为了记录,法师/法术概念只是我正在研究的一个更复杂问题的一个简单示例。虽然复合法术的概念相当有趣……不过这确实很有帮助,谢谢。我认为这是我正在掌握但无法完全形成的解决方案。
  • 不管怎样,这些概念都是广泛适用的。毕竟是命令 pattern :)
猜你喜欢
  • 2023-03-12
  • 2014-12-11
  • 1970-01-01
  • 1970-01-01
  • 2018-08-10
  • 1970-01-01
  • 2022-11-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多