Professional C__ - Marc Gregoire [489]
Because you don’t want to expose network problems to the user, it may be desirable to have a separate class that hides the networked parts of a Player. This PlayerProxy object would substitute for the actual Player object. Clients of the class would either use the PlayerProxy class at all times as a gatekeeper to the real Player class, or the system would substitute a PlayerProxy when a Player became unavailable. During a network failure, the PlayerProxy object could still display the player’s name and last-known state, and could continue to function when the original Player object cannot. Thus, the proxy class hides some undesirable semantics of the underlying Player class.
Implementation of a Proxy
The public interface for a Player class follows. The sendInstantMessage() method requires network connectivity to properly function:
class Player
{
public:
virtual string getName();
// Sends an instant message to the player over the network and
// returns the reply as a string. Network connectivity is required.
virtual string sendInstantMessage(const string& inMessage) const;
};
Proxy classes often evoke the is-a versus has-a debate. You could implement PlayerProxy as a completely separate class that contains a Player object. This design would make most sense if the PlayerProxy is always used by the program when it wants to talk to a Player object. Alternatively, you could implement PlayerProxy as a subclass that overrides functionality that requires network connectivity. This design makes it easy to swap out a Player for a PlayerProxy when network connectivity ceases. This example uses the latter approach by subclassing Player, as shown here:
class PlayerProxy : public Player
{
public:
virtual string sendInstantMessage(const string& inMessage) const;
};
The implementation of the PlayerProxy’s sendInstantMessage() method simply cuts out the network functionality and returns a string indicating that the player has gone offline.
string PlayerProxy::sendInstantMessage(const string& inMessage)
{
return "The player could not be contacted.";
}
Another solution could be for the PlayerProxy’s sendInstantMessage() method to check the network connectivity, and either return a default string or forward the request. For example:
string PlayerProxy::sendInstantMessage(const string& inMessage)
{
if (hasNetworkConnectivity())
return Player::sendInstantMessage(inMessage);
else
return "The player could not be contacted.";
}
Using a Proxy
If a proxy is well written, using it should be no different from using any other object. For the PlayerProxy example, the code that uses the proxy could be completely unaware of its existence. The following function, designed to be called when the Player has won, could be dealing with an actual Player or a PlayerProxy. The code is able to handle both cases in the same way because the proxy ensures a valid result.
bool informWinner(const Player* inPlayer)
{
string result;
result = inPlayer->sendInstantMessage("You have won! Play again?");
if (result == "yes") {
cout << inPlayer->getName() << " wants to play again" << endl;
return true;
} else {
// The player said no, or is offline.
cout << inPlayer->getName() << " does not want to play again" << endl;
return false;
}
}
THE ADAPTER PATTERN
The motivation for changing the abstraction given by a class is not always driven by a desire to hide functionality or protect against performance concerns. Sometimes, the underlying abstraction cannot be changed but it doesn’t suit the current design. In this case, you can build an adapter or wrapper class. The adapter provides the abstraction that the rest of the code uses and serves as the bridge between