6

少し前に、ゲームの戦闘コードをリファクタリングする際に、デコレータ パターンを試すことにしました。戦闘員はさまざまな受動的能力を持つことができ、さまざまな種類のクリーチャーである可能性もあります。デコレーターを使用すると、実行時にさまざまな組み合わせで動作を追加できるので、何百ものサブクラスは必要ないと考えました。

パッシブ アビリティ用の 15 個ほどのデコレータの作成がほぼ完了しました。テスト中に、これまで聞いたことのないデコレータ パターンの明らかな欠点を発見しました。

デコレーターが機能するには、最も外側のデコレーターでメソッドを呼び出す必要があります。「基本クラス」 (ラップされたオブジェクト) が独自のメソッドの 1 つを呼び出す場合、呼び出しをラッパーに「仮想化」する方法がないため、そのメソッドは装飾されたオーバーロードにはなりません。人工サブクラスの概念全体が崩壊します。

これは大したことです。私の戦闘員は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オーバーライドできます。たとえば、ダメージを 4 分の 1 にカットするパッシブや、ダメージを攻撃者に反射するパッシブなどです。

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

しかし、繰り返しになりますが、これらのデコレータ メソッドは、外部から呼び出されるのではなく、基底クラス内で呼び出されるため、呼び出されることはありません。

4

2 に答える 2

3

tl; dr:デコレータは、オブジェクトまたは関数の動作を変更することを目的としていますが、サブクラス化のように元の動作をオーバーライドすることはありません。

「基本クラス」(ラップされたオブジェクト)が独自のメソッドの1つを呼び出す場合、その呼び出しをラッパーに「仮想化」する方法がないため、そのメソッドは装飾されたオーバーロードにはなりません。人工サブクラスの概念全体が崩壊します。

私が正しく理解しているなら、あなたはこれを言っている-

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(){
       ...
    }
}

しかし、これは多かれ少なかれパターンの精神を壊します。内部オブジェクト自体を変更する場合は、ゲッターとセッターを使用してインターフェイスにメソッドを作成する必要があります。これもパターンの精神を多少損なうものですが、場合によっては機能する可能性があります。

于 2012-08-19T20:43:32.453 に答える
1

実際、デコレータ パターンは問題に対する適切な解決策ではありません。デコレーターは、既存の動作を変更するのではなく、既存の実装の上に透過的に機能を追加することのみを目的としています。

より適切と思われる他の 2 つのパターンは次のとおりです。

戦略パターン:追加機能を にラップしようとする代わりにCombatantCombatant自体がさまざまな戦略に委譲して、パズルのさまざまなピースを計算します。例えば:

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
}
于 2012-08-20T01:04:23.863 に答える