Before letting you read on I find that I must give readers a strong warning lest we be accused of breach of the Intellectual Property Rights of the owners of Monopoly®. The source code provided by these articles must never be used to produce a working electronic version of Monopoly®. The game is being used purely for the purposes of providing an exposition of certain computer programming techniques and for no other purpose.
Francis Glassborow Journal Editor, ACCU
In the previous article we looked at my original C++ implementation of Monopoly®, and pointed out a few of the things that were incorrect in the design. As I mentioned in the first article, I am now using this 'Pet Project' to teach myself something about UML and "try-out" a few patterns. Before we dig deeper into these areas I wish to say a word about what prompted me to write these articles.
Francis. Well, actually it was the Whiteboard article Francis wrote in Overload (see Ref [ Glassborow ]). He had been studying the patterns in Design Patterns (the GOF book, Ref [ GoF ]), and had been writing up the results of his investigations. In the article he stated he has now decided to "call it a day" because he is "concerned with understanding and not just a superficial ability to imitate".
Well, I make no bones about it, this article is pure imitation. It discusses my experiences of the Proxy , Bridge and Visitor patterns from the above book. I really do not know if they are used "correctly" - so don't take this as a "lesson". One aim is to show how these experiences worked (or otherwise). A second aim is to convey ideas about how patterns may be put into practice, rather than show complete implementations (so do not go looking for an assignment operator in the Proxy and Bridge for example).
First experiences
My first introduction to patterns was the GOF book (Ref [ GoF ]). I read it. It made sense (really). I was keen to apply what I'd read. And that's where I hit a brick wall. I thought I could see how to apply patterns to some parts of the Monopoly program, but when I tried them I felt my original implementation still to be reasonable (however crude it was with all the globals). I found it difficult to apply any of the patterns and end up with a "better" implementation. I re-read the book. It still made sense. I bought other books (Refs [ Booch ], [ Martin ], [ Fowler ] and [ Buschmann- ]) (and read them, though not yet cover to cover). They all helped but I still couldn't see any way to seriously apply a pattern.
Something was missing - and it turned out to be one of my most recent purchases - Pattern Hatching (Ref [ Vlissides ]). This has shed a light on how to apply patterns; the question remains "Are they being applied correctly?"
Continuing thoughts on STL
In the last article a statement was made about the Player and Site classes in Monopoly. That is, they need to be reference-based objects. If they were value based then the following piece of code would not do what was expected:
void Bank::CreditPlayer(Player p, int amount){ p.cash += amount; } void Board::PassGo(){ int const salary = 200; theBank.CreditPlayer(currentPlayer, salary); }
A copy of the currentPlayer object would be credited with the salary , not the currentPlayer itself. We force the Player objects to be reference based by declaring the copy constructor and assignment operator to be private and not defining them. If we now try to call the CreditPlayer method we end up with error message during compilation to say that Player 's copy constructor cannot be accessed.
Passing Player by reference to CreditPlayer ( void Bank::CreditPlayer (Player& p, int amount) ), not only gets rid of our compilation error but also means the routine achieves the expected result.
However, making Player objects reference based also leads to another problem if we start using the standard template library. The STL container classes require value-based objects. If we want to use Player in std::list<Player> then we get the same 'copy constructor' error message as mentioned previously.
At first sight there appear to be three [ 1 ] ways forward:
-
using pointers;
-
using the Proxy pattern;
-
using the Bridge pattern [ 2 ] .
1. Pointers
Wherever we wish to add a Player object to a list we do so by making the list entry refer to the object's address, i.e. we have std::list<Player*> . It works [ 3 ] , but one ends up with 'ugly' code having to de-reference or take addresses of objects whenever we use the list. We also enter an area of my own uncertainty - what happens when Player is becomes an abstract class and has HumanPlayer and ComputerPlayer derived from it. I have a niggling suspicion that this could end up with some implementation dependent code if we were comparing object addresses.
2. Proxy
GOF definition of the Proxy pattern states that it "provides a surrogate or placeholder for another object to control access to it." Well this sounds like what is needed - if we have a PlayerProxy , which is value-based, and it and its copies are the placeholders for Player , we could declare it thus:
class Player; class PlayerProxy { public: PlayerProxy(Player& p) : myPlayer(p) {}; PlayerProxy(PlayerProxy const& p) : myPlayer(p.myPlayer) {} operator Player& () { return myPlayer; } private: Player& myPlayer; };
We can then use PlayerProxy whenever we want to put Player s in an STL container. Note that the operator Player&() provides a mechanism to get the original Player object when its required. However we talked about 'ugly' code when using pointers and the above still requires us to deal with proxy objects separately from the original object - see example at top of next column:
Player me ("Nigel"); PlayerProxy myProxy(me); // Here, std::list<PlayerProxy> thePlayers; // here thePlayers.push_back(myProxy); // and here we deal with the Proxy class or object. Player& currentPlayer(thePlayers.front()); // but we can get the original!
Another problem with this solution is that, if we're not careful, it is possible for the proxy object to reference a non-existent player:
PlayerProxy CreatePlayer(std::string name) { Player p(name); PlayerProxy pp(p); return pp; } foo { std::list<PlayerProxy> thePlayers; thePlayers.push_back(CreatePlayer("Nigel")); // but the Player object no longer exists! }
It would be much nicer if we could treat the proxy object as if it was the real player (but value-based) and let it control creation, lifetime and access to the real reference-based Player object. So I'm after a PlayerProxy , which controls the interface to a Player but the Player performs the actual implementation.
I've already stated I want to treat the proxy object as if it was the real player, so from now on I rename the classes. I shall use the class names Player and PlayerImplementation . Player now represents value-based objects; PlayerImplementation represents the reference-based object.
3. Bridge
Warning: I now believe my next steps to be wrong, but I'm relating my experiences and this proved a slight, but nevertheless useful diversion.
Design Patterns (Ref [ GoF ]) describes Bridge thus "Decouple an abstraction from its implementation so that the two can vary independently".
So my (incorrect) train of thought was, "An abstract class (or better a pure abstract class) defines an interface, so let's define an interface but, rather than make it abstract, let's give it a bit of intelligence and responsibility about the creation of the implementation object". The interface class would be value-based, but only one 'implementation' object would get created. Without thinking too deeply we end up with something like:
class Player { public: Player(std::string name) : myPlayerImp( new PlayerImplementation(name)){} Player(Player const& p) : myPlayerImp(p.myPlayerImp) {} ~Player() {delete myPlayerImp; } int GetCash() { return myPlayerImp->GetCash(); } // etc private: PlayerImplementation* myPlayerImp; }; class PlayerImplementation { public: PlayerImplementation (std::string name) : myName(name), myCash(1500) {} int GetCash (void) { return myCash; } // etc private: PlayerImplementation (PlayerImplementation const&); PlayerImplementation& operator= (PlayerImplementation const&); private: std::string myName; int myCash; };
Of course we ought to think further! What happens in the following (rather contrived) case?
Player CreatePlayer (std::string name) { Player p (name); return p; } foo { Player aNewPlayer (CreatePlayer ("Nigel")); }
Well, the creation of object p will create the implementation object then, on exit from CreatePlayer , p gets destroyed and the implementation object gets destroyed. We end up with aNewPlayer.myPlayerImp pointing to a non-existent PlayerImplementation object. We only want to destroy the PlayerImplementation when the final Player object, referencing it, is being destroyed. So we need to add a reference counting mechanism. I will not do this here - it's more than adequately covered by Scott Meyers in Ref [ Meyers ].
There's one more important point to note here. It goes back to my statement about giving the interface ("abstract") class Player "a bit of intelligence and responsibility". By doing this I'm not using the Bridge pattern at all. In fact I've simply turned it into a better Proxy pattern than my previous example. The differences from the original are:
-
The choice of names for the classes (The "proxy" class is now called Player and the real player objects are of class PlayerImplementation )
-
Control over how the real player object is created and destroyed.
If you wish to read more about the Bridge pattern and decoupling abstraction from implementation, then the case study in Ref [ Martin ] provides many examples of this.
Well this series of articles started out as a trilogy, but I'll look at the Visitor pattern and mention a quick word about UML and Monopoly in the fourth and final part of the trilogy!
References
[Glassborow] Ruminations on Studying Design Patterns ( Overload 32), Francis Glassborow; ISBN 1354-3172
[GoF] Design Patterns - Elements of reusable software , Gamma et al; Addison Wesley; ISBN 0-201-63361-2
[Booch] Object-oriented Analysis and Design , Grady Booch; Benjamin / Cummings; ISBN 0-8053-5340-2
[Martin] Designing Object Oriented C++ Applications Using the Booch Method , Robert Martin; Prentice Hall; ISBN 0-13-203837-4
[Fowler] Analysis Patterns - Reusable Object Models , Martin Fowler; Addison Wesley; ISBN 0-201-89542-0
[Buschmann-] A System of Patterns (Pattern - Oriented Software Architecture) , Buschmann et al; Wiley; ISBN 0-471-95869-7
[Vlissides] Pattern Hatching - Design Patterns Applied , John Vlissides; Addison Wesley; ISBN 0-201-43293-5
[Meyers] More Effective C++ , Scott Meyers; Addison Wesley; ISBN 0-201-63371-X
[ 1 ] I wonder if another way would be to encapsulate the STL container in a class that provides a reference based interface. I think this is the way the STL is intended for use in high level code. Francis Glassborow
[ 2 ] Experienced 'Patternites' bear with me on this one.
[ 3 ] Or should I say "appears to work"?