The Decorator pattern explained

The purpose of the Decorator pattern is to dynamically add responsibilities to an object at runtime. The goal is to be a flexible alternative to creating derived classes while still allowing for extended behavior. What this means is that we can take our object and add decorations or, in our case, behaviors at runtime.

This pattern requires that the Decorator and our object are derived from a common base class so they share the same interface. Each Decorator will then layer itself on top of an object or another Decorator to create more interesting object types and effects. When a function gets called on a Decorator, it will call the corresponding function on the next layer down, eventually calling the function of the object. It is similar in concept to the Russian Matryoshka dolls, the dolls that contain smaller and smaller versions inside of themselves. The final, most nested object is always the object with the core functionality:

Figure 3.7 - The layering effects of the Decorator pattern

Here is a simplified version in code:

class Component //Our base interface 
{
public:
virtual ~Component(void) {}
virtual std::string Describe(void) const = 0;
};

class Object: public Component //Our core class to decorate
{
public:
Object(const std::string& name):m_name(name){}
virtual std::string Describe(void) const
{
return m_name;
}
private:
std::string m_name;
};

//Our base and derived Decorators
class Decorator: public Component
{
public:
Decorator(Component* comp):m_comp(comp){}
virtual ~Decorator(void) { delete m_comp; }
protected:
Component* m_comp;
};

class RocketBoosters: public Decorator
{
public:
RocketBoosters(Component* comp) : Decorator(comp) {}
virtual std::string Describe(void) const
{
return m_comp->Describe() + " with RocketBoosters";
}
};

class LaserCannons: public Decorator
{
public:
LaserCannons(Component* comp) : Decorator(comp) {}
virtual std::string Describe(void) const
{
return m_comp->Describe() + " with LaserCannons";
}
};
Figure 3.8 - The Decorator pattern using our Object
//Using this code: 
int main(void)
{
Component* ship = new Object("Player");
std::cout << ship->Describe() << std::endl;
delete ship;

Component* rocketShip = new RocketBoosters(new
GameObject("Enemy"));
std::cout << rocketShip->Describe() << std::endl;
delete rocketShip;

Component* laserRocketShip = new LaserCannons(new
RocketBoosters(new GameObject("Boss")));
std::cout << laserRocketShip->Describe() << std::endl;
delete laserRocketShip;
}

The Decorator classes layer our concrete object class and add more information on top of the object. However, right now, all we are doing is adding superficial decorations. Since the Decorator class doesn't know whether it has a pointer to the object class or another Decorator, it can't modify the object. A good analogy is that the Strategy pattern changes the guts of the object, while the Decorator pattern changes the skin. This can be useful but doesn't help us with our buff/debuff problem. To solve this problem, we would need to add a method to find the object down the chain, or give a pointer to the object in the constructor of a Decorator.

Another problem is that this pattern was designed to add a Decorator dynamically, but doesn't allow us to remove one. In the case of using a corrosive damage Decorator, we would only want it to exist for a set time, and then automatically detach itself. This can't be done, since a Decorator doesn't have a pointer to its parent.

The final problem for games is that our Decorators can't live in a vacuum. Sometimes, different gameplay behaviors may need to interact with each other. For example, the corrosive damage Decorator may affect the health of an object; however, it may first need to check whether the object has a shield Decorator and remove health from the shield.

Unfortunately, neither the Decorator nor the Strategy pattern will work perfectly for us. What we really need is a new pattern that is a combination of the Strategy and Decorator patterns that does the following:

  • Encapsulates specific behavior into components so we avoid Object inheritance trees
  • Allows for a flexible number of components so we don't need to modify the Object each time we create a new component type
  • Lets us add and remove components at runtime
  • Gives components direct access to the Object so it can be modified
  • Allows components to be searchable by other components so they can interact
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.139.169