纠正装饰器图案的大缺点

时间:2012-08-19 19:56:18

标签: php design-patterns decorator

我决定在重构一些游戏战斗代码时,尝试装饰模式。战斗员可以拥有各种被动技能,也可能是不同类型的生物。我认为装饰器允许我在运行时添加各种组合的行为,所以我不需要数百个子类。

我差不多完成了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类中。 DamageReductionDamage可以在各种装饰器中被覆盖,例如被动器可以将伤害减少四分之一,或者另一个可以将一些伤害反射回攻击者。

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;
    }
}

但同样,这些装饰器方法永远不会被调用,因为它们不是从外部调用的,而是在基类中调用它们。

2 个答案:

答案 0 :(得分:2)

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编写方法,这或多或少会破坏模式的精神,但可能适用于你的情况。

答案 1 :(得分:1)

装饰器模式确实不是解决问题的正确方法。装饰器仅用于在现有实现之上透明地添加功能,而不是修改现有行为。

其他两种似乎更合适的模式是:

Strategy pattern Combatant本身不会尝试将其他功能包装到Combatant上,而是委托各种策略来计算拼图的不同部分。例如:

interface DamageReductionStrategy {
  public function computeDamageReduction(Combatant $combatant);
}

class Combatant {
  private $damageReductionStrategy;

  public function TakeHit($attacker, $quality, $damage) {
    $damage -= $this->damageReductionStrategy->computeDamageReduction($this);
    ...
  }
}

Observer pattern 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
}