【问题标题】:Correcting a large disadvantage to the decorator pattern纠正装饰器模式的一大缺点
【发布时间】:2012-08-15 07:15:45
【问题描述】:

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

我几乎已经完成了为被动技能制作 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;
    }
}

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

【问题讨论】:

  • 你能发布一些代码吗?
  • 你能发布完整的列表吗?如果要覆盖 Damage 和 DamageReduction,则需要适当地调用包装类
  • @dfb 我愿意,就像在你的回答中一样,装饰器只是调用所有包装的方法。
  • 我今天正面临着​​确切的问题。我找到的唯一解决方案是 CQRS,基本上意味着一类 - 一种方法。因为一旦你有两个,一个调用另一个,整个装饰器就停止工作了。

标签: php design-patterns decorator


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    另外两种看起来更合适的模式是:

    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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-07
      相关资源
      最近更新 更多