The Observers

For this example, we have the three Observers that we mentioned before. Two are related to displaying the health of the Player in different ways; the other is used for quitting when the health of the Player is zero or less:

//Used to quit the game when the "game", when the player's health 
//is less than or equal to 0
class StageLogic : public Observer
{
public:
StageLogic (Subject* pSubject);
bool IsQuitting(void) const;
~StageLogic(void);
virtual void Update(float currentHealth, float maxHealth);
private:
bool m_isQuitting;
Subject* m_pSubject;
};

//Used to Color the player health bar based on the how full it is
class PlayerHealthBar : public Observer
{
public:
PlayerHealthBar(Subject* pSubject);
~PlayerHealthBar(void);
void Display(void) const;
virtual void Update(float currentHealth, float maxHealth);
private:
float m_percent;
std::string m_color;
Subject* m_pSubject;
};

//Used to Display the health of the player as a value
class PlayerDisplay : public Observer
{
public:
PlayerDisplay(Subject* pSubject);
~PlayerDisplay(void);
void Display(void) const;
virtual void Update(float currentHealth, float maxHealth);
private:
float m_health;
Subject* m_pSubject;
};

As you can see, each derived Observer class overloads the Update method from base. You will also notice that each constructor takes a pointer to a Subject as the only parameter, and saves that pointer into a member variable. This isn't necessary, but it makes registering and unregistering more convenient because the objects take care of themselves. In this example, all three of the Observers' constructors and destructors do the exact same thing. Here is one:

PlayerDisplay::PlayerDisplay(Subject* pSubject): 
m_health(0.0f),
m_pSubject (pSubject)
{
m_pSubject ->RegisterObserver(this);
}

PlayerDisplay::~PlayerDisplay(void)
{
m_pSubject ->UnregisterObserver(this);
}

The choice of keeping a pointer to the Subject is up to you. It has some problems, which we will look at a little later; however, it allows the Observer to unregister in the destructor. This means that the user doesn't need to do it, which makes using the Observers classes very easy. If we don't keep this pointer, unregistering must be done manually and could be difficult depending on how you access the Subject and Observers.

The rest of the Observer methods are simple and don't interact with the Subject at all. Instead, the Update methods do some logic based on the values of currentHealth and maxHealth. For the two display elements, this means calculating some values; for the StageLogic class, this means setting m_isQuitting to true if the value of current health is zero or less. Let's look at an example Update from one of the Observers:

void PlayerHealthBar::Update(float currentHealth, float maxHealth) 
{
m_percent = (currentHealth / maxHealth) * 100.f;

if (m_percent >= 75.0f)
m_color = "Green";
else if (m_percent < 75.0f && m_percent > 35.0f)
m_color = "Yellow";
else
m_color = "Red";

}
Figure 9 1 Interaction of Subjects and Observers

As you can see, the Update methods aren't very complicated. Nothing about the above method is using the Subject. The data could have come from anywhere. The part that is most interesting is how simple these objects are to use now. All three Observers are using the Player's health, but they don't need to call any Player methods. Even though these four objects interact, using them is incredibly simple. Let's look at how we can use these objects together:

int main(void) 
{
//Our value to decrement by
const float DECREMENT = -1.0f;
const float STARTING_HEALTH = 5.0f;

//creating our objects
Player player(STARTING_HEALTH);
PlayerDisplay display(&player);
PlayerHealthBar bar(&player);
StageLogic stageLogic(&player);

//Set the initial values to print
player.Notify();

//loop until player is dead
while (!stageLogic.IsQuitting())
{
display.Display();
bar.Display();
player.AdjustHealth(DECREMENT);
}

return 0;
}

The main function starts out with a few const values to improve readability. After that, we create our objects. We first create our Player, which is our Subject. Then we create our Observers. Each Observer gets a pointer to the Subject. Remember, they are only dependent on the Subject interface, not to the derived Player class. Once all the Observers are created, the Player does an initial Notify call so the Observers start out with the correct data. Finally, we use our objects. The simplicity of this while loop is amazing. Since the code linking them together is all internal, using the objects together becomes very easy. Compare the example above with a version of the code that doesn't use the Observer pattern:

  //Alternate code without Observer pattern 
while (!stageLogic.IsQuitting())
{
display.SetHeath(player.getHealth());
display.Display();

bar.setHealth(player.getHealth(), player.getMaxHealth());
bar.Display();

player.AdjustHealth(DECREMENT);

stageLogic.SetQuit(player.GetHealth() <= 0);
}

Using the Observer pattern allows us to write code that is more elegant and simple to use. Unfortunately, many programmers write code that is closer to the second version. They don't realize that with just a little thought as to how the objects will interact, the code is easier to use, easier to read, and is more efficient because it only gets data from the Player when the data changes.

In this simple example, it may not seem like the code is much different, but remember that this is just demonstrating the pattern as simply as possible. The second version looks reasonable because all the objects are in the same scope. Except for the constructor, in a real project, the Observer code stays the same. However, the second version can become a mess of Singleton method calls and object look ups.

..................Content has been hidden....................

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