不久前,在重构一些游戏战斗代码时,我决定尝试装饰器模式。战斗人员可以拥有各种被动能力,也可能是不同类型的生物。我认为装饰器允许我在运行时以各种组合添加行为,因此我不需要数百个子类。
我几乎完成了 15 个左右的被动技能装饰器的制作,在测试中我发现了一些东西——装饰器模式的一个相当明显的缺点,我很惊讶我以前从未听说过。
为了使装饰器完全工作,必须在最外层的装饰器上调用它们的方法。如果"基类"(包装的对象(调用自己的方法之一,则该方法不会是修饰的重载,因为无法将调用"虚拟化"到包装器。人工子类的整个概念被打破了。
这有点大不了。我的战斗人员有像TakeHit
这样的方法,而这些方法又称为他们自己的Damage
方法。但是装饰过的Damage
根本没有被召唤。
也许我选择了错误的模式,或者对其应用过于热心。在这种情况下,您是否对更合适的模式有任何建议,或者解决此缺陷的方法?我重构的代码只是将所有被动能力洒在看似随机的地方if
块内的战斗代码中,所以这就是我想打破它的原因。
编辑:一些代码
public function TakeHit($attacker, $quality, $damage)
{
$damage -= $this->DamageReduction($damage);
$damage = round($damage);
if ($damage < 1) $damage = 1;
$this->Damage($damage);
if ($damage > 0)
{
$this->wasHit = true;
}
return $damage;
}
此方法位于基Combatant
类中。 DamageReduction
和Damage
可以并且都在各种装饰器中被覆盖,例如将伤害降低四分之一的被动,或者将一些伤害反射回攻击者的被动。
class Logic_Combatant_Metal extends Logic_Combatant_Decorator
{
public function TakeHit($attacker, $quality, $damage)
{
$actual = parent::TakeHit($attacker, $quality, $damage);
$reflect = $this->MetalReflect($actual);
if ($reflect > 0)
{
Data_Combat_Event::Create(Data_Combat_Event::METAL_REFLECT, $target->ID(), $attacker->ID(), $reflect);
$attacker->Damage($reflect);
}
return $actual;
}
private function MetalReflect($damage)
{
$reflect = $damage * ((($this->Attunement() / 100) * (METAL_REFLECT_MAX - METAL_REFLECT_MIN)) + METAL_REFLECT_MIN);
$reflect = ceil($reflect);
return $reflect;
}
}
但同样,这些装饰器方法永远不会被调用,因为它们不是从外部调用的,而是在基类内部调用的。
tl;dr:装饰器旨在更改对象或函数的行为,但它不会像子类化那样覆盖原始行为。
如果"基类"(包装的对象(调用自己的一个 方法,该方法不会是修饰的重载,因为没有 将调用"虚拟化"到包装器的方式。整个概念 的人工子类分解。
如果我理解正确,你说的是——
decorated_thingy_instance = DecoratorA(OriginalThingy))
鉴于
DecoratorA{
decoratedThingy = ...;
doStuff(){
decoratedThingy.doStuff()
...
}
doOtherStuff(){
decoratedThingy.doOtherStuff()
...
}
}
和
OriginalThingy{
doStuff(){
this.doOtherStuff()
}
doOtherStuff(){
...
}
}
你的问题是DecoratorA的doOtherStuff没有被调用。 最好考虑应用于函数而不是对象的装饰器,它不完全像子类。 原则上,每个装饰器的行为不应影响其他装饰器或内部对象的行为,原因与您提到的相同,您不能像更改子类一样更改控制流。
实际上,这意味着您可以更改接口公开的函数的结果(将输出乘以 2(,但不能更改包装类计算函数的方式。 你可以制作一个包装器,完全丢弃包装类的输出,或者完全不调用它,例如,
DevNullDecorator{
decoratedThingy = new Thingy();
doStuff(){
//decoratedThingy.doStuff()
// do whatever you want
}
doOtherStuff(){
...
}
}
但这或多或少打破了模式的精神。 如果你想修改内部对象本身,你需要在接口中编写带有getter和setter的方法,这也或多或少地破坏了模式的精神,但可能适用于你的情况。
装饰器模式确实不是解决问题的正确方法。装饰器只是为了在现有实现之上透明地添加功能,而不是修改现有行为。
另外两种看起来更合适的模式是:
策略模式:Combatant
本身不会试图将附加功能包装到Combatant
上,而是委托给各种策略来计算拼图的不同部分。例如:
interface DamageReductionStrategy {
public function computeDamageReduction(Combatant $combatant);
}
class Combatant {
private $damageReductionStrategy;
public function TakeHit($attacker, $quality, $damage) {
$damage -= $this->damageReductionStrategy->computeDamageReduction($this);
...
}
}
观察者模式:Combatant
可以在发生各种事件时提供钩子(侦听器(,以便可以采取其他操作。例如:
interface DamageListener {
public function hitTaken($combatant, $attacker, $quality, $damage);
}
class Combatant {
private /* array */ $damageListeners;
public function TakeHit($attacker, $quality, $damage) {
...
foreach ($this->damageListeners as $listener)
$listener->hitTaken($this, $attacker, $quality, $damage);
}
}
class MetalReflect implements DamageListener {
// code to reflect damage
}