Correcting a large disadvantage to the decorator pattern

Viewed 966

I decided a while back, in refactoring some game combat code, to try the decorator pattern. The combatants can have various passive abilities, and they may also be different types of creatures. I figured that decorator lets me add on behavior in various combinations at run time so I don't need hundreds of subclasses.

I've almost finishing making the 15 or so decorators for the passive abilities, and in testing I discovered something - a rather glaring disadvantage to the decorator pattern that I'm surprised I haven't heard of before.

For the decorators to work at all, their methods must be called on the outermost decorator. If the "base class" - the wrapped object - calls one of its own methods, that method won't be the decorated overload, since there's no way for the call to be "virtualized" to the wrapper. The whole concept of an artificial subclass breaks down.

This is kind of a big deal. My combatants have methods like TakeHit which in turn call their own Damage method. But the decorated Damage isn't getting called at all.

Perhaps I've chosen the wrong pattern or been overzealous in its application. Do you have any advice on a more appropriate pattern in this situation, or a way to work around this flaw? The code that I refactored from just had all the passive abilities sprinkled all over the combat code inside if blocks in seemingly random places, so that's why I wanted to break it out.

edit: some code

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

This method is in the base Combatant class. DamageReduction and Damage can and are both overridden in various decorators, for example a passive that cuts damage by a quarter, or another that reflects some damage back to the attacker.

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

But again, these decorator methods are never getting called, because they aren't being called from the outside, they're called inside the base class.

2 Answers
Related