I had initially intended to write three articles on my experiences with my pet project (Monopoly ®) but, as you can see, the trilogy has become a tetralogy.
In parts two and three a number of points were left 'until the next article'. I'm now hoping to wrap things up and talk about these remaining points, which relate to:
-
Object construction;
-
Class design;
-
Model-View-Controller pattern;
-
Visitor pattern;
-
UML.
As you will see they cover a lot of discordant territory and topics do not necessarily have the nice clean wrap-up I was hoping for[1]. I hope the reasons for this become evident toward the end of the article.
Part two raised the issue of constructing objects of class Site and ColourGroup. It stated that the following constructors would not be possible:
class AssetSite { public: AssetSite (ColourGroup&, etc…); }; class ColourGroup { public: ColourGroup (AssetSite&, AssetSite&); // Utility, Mayfair & OldKentRd ColourGroup (AssetSite&, AssetSite&, AssetSite&); // Most BuildingSites ColourGroup (AssetSite&, AssetSite&, AssetSite&, AssetSite&); // Stations };
because an asset site cannot be created until its colour group exists, and vice versa.
There can also be other problems related to construction, one of which is related to the chance and community chest cards. Imagine the following classes in the Monopoly implementation:
- Card
-
abstract class for chance and community chest cards;
- CardPack
-
a list of Cards;
- CardSite
-
a site on the board that, when a player lands on the site, picks a card from an CardPack list and requests that Card to perform an action on behalf of the current player. (See part 2, figure 3)
Now one example of a card is "Advance to Trafalgar Square. If you pass Go collect £200". This type of class was represented by the MoveRoundCard, (which derives from Card). Let us assume it has the following constructor:
MoveRoundCard (Site& siteToAdvanceTo);
Let us also assume the CardSite has the constructor:
CardSite (CardPack& thePack);
Now in what order do we construct these objects? Well, we have to construct the trafalgarSquare site before the advanceToTrafalgarSquare card, as it will get referenced in the card's constructor. Also we have to create the CardPack before the site's as the packs get referenced in the CardSite's constructor. We end up therefore with the following construction sequence:
// CardPacks // Sites // Cards // Add cards into their cardPack.
Well it work's, but I find the whole thing ugly, because you refer to CardPacks, then Sites, then Cards and finally back to CardPacks again. There is a lot of jumping around and that kind of code gives me an uneasy feeling in the stomach.
When you start considering assigning TitleDeed objects to their Site and/or vice-versa, handing out pre-dealt TitleDeeds to Players, adding Players onto their starting Site, relating TitleDeeds to a ColourGroup etc, the whole construction of objects at the start of a Monopoly game gives me nightmares.
I have no immediate answers to this, just a few thoughts that feel like they might be considered. (Maybe someone has already tackled a similar problem and would like to write up their experiences).
The Design Patterns book, ref [GoF], describes the intent of the Builder pattern as: "To separate the construction of a complex object from its representation so that the same construction process can create different representations."
It seems a laudable intent just to "separate the construction of a complex object from its representation", regardless of the fact it has different representations or not.
I have not yet experimented with the use of the Builder pattern so cannot really comment further but, although Builder keeps the construction process separate, it does not undo any of the complexity of the construction process.
A Builder pattern will probably get considered in my UML version - see later comment - and we will see then how successful it is.
It has already been mentioned about the dilemma of construction of ColourGroups and AssetSites. What happens when we have default constructors for these classes? We must fix-up attributes or associations after the object's construction.
So we might have:
AssetSite oldKentRd; AssetSite whitechapel; ColourGroup darkBrownColourGroup; oldKentRd.name = "Old Kent Road"; oldKentRd.colourGroup = darkBrownColourGroup; whitechapel.name = "Whitechapel"; whitechapel.colourGroup = darkBrownColourGroup; darkBrownColourGroup.Add (oldKentRd); darkBrownColourGroup.Add (whitechapel);
I will not go on at this point. As you can see it is getting long winded. It could be made a little simpler by using an Initialise method rather than do attributes one by one. Anyhow my gut feeling still is not happy with this approach so I will dismiss it in the belief that there must be a better way.
The original coupling came about because, when landing on a Site an Action[2] was required, e.g. when landing on an AssetSite the Action required is to collect rent on behalf of the Site's owner. In this case the Site needs to know about all the other Sites in the same colour group because if they are all owned by the same owner then the rent may get doubled.
If the Action of rent collection is handled differently then the Site may not need any knowledge of the colour group. We will be looking at this further in this article when the Visitor pattern is discussed. It will also get considered during the UML design.
In the original implementation the AssetSite class actually has two uses. In one instance it was used for a site that had visitors and, when a player lands on the site some action occurs (collecting rent). In the other instance they represented an Asset owned by a player.
We can deal with the first use quite quickly, as we have already briefly mention decoupling and the possibility of executing the Action in a different manner. Hopefully it will resolve itself that - in addition to a Site not requiring knowledge of its ColourGroup - it may not need to know about its visiting Players.
Figure one shows one possibility of separating an AssetSite and TitleDeed. What advantages could this bring? Well houses and hotels are assets, a player's cash is an asset and even a get out of jail free card could be a useful asset at some stage. This is shown in figure two.
This would be useful when tallying a player's assets from a std::set<Asset*> for example, where it is not necessary to worry about the type of the asset, merely its value.
One of the issues I did not like with the original implementation was how information regarding the current state of play was stored and which parts dealt with the control of the game.
Basically the current state of a game was held in a number of global variables, which were accessed by whichever class needed the information.
The purpose of model-view-controller is to separate the data from user control and views of that data.
Initially it seems logical to wrap the global data into a class (MonopolyDocument for example) that is responsible for not only holding the game state, but also saving and restoring the data to / from a file. This defines the model for the game.
Any view of the game simply reads the data from the document. This is probably a case for the Observer pattern, whereby the views are observers of the MonopolyDocument subject (see ref [GoF]).
Control of that data seems a little more complicated. It could be performed through a GameManager, which is responsible for keeping the data in the MonopolyDocument in a consistent state. Any menu-item clicks or button pushes correspond to a call of a GameManager method. But this is not as simple as it sounds - let us take a look at the action required when a player lands on a community chest or chance site. When this occurs the current player must obey the instructions on the card.
For most cards this is easily handled, but sometimes the player must make a choice (buy or sell the "Get out of Jail free" card). In all cases it is necessary to display the card to the actual player so they know what is happening.
So what is responsible for creating the view of the card? If the GameManager is truly separated from views then it should not create the window. The game might be running as a console application, Windows application or even an Internet application so who knows how the card gets displayed to the player? Also, as can be seen from figure 3, the view and controller classes are completely separated.
But this would mean that each of the states - a) picking up the card, b) choosing a response[3], c) performing the action and d) replacing the card - would have to be known by and recorded in the document. This seems a bit extreme.
Ref [GoF] indicates that each View has an instance of a Controller (figure 4). The reason given is so that views can give different responses simply by using different controllers. This is not quite the situation we are looking at, but it may be the Strategy pattern (the interaction between Controller / View) may help in performing the action required when landing on a CardSite.
Yet again I shall leave any further decision making here until it gets reconsidered during my UML design.
In the second article I described what happens when a player lands on a site or picks up a chance / community chest card. The site and card[4] classes have a virtual function (called Action) which gets called. That function is then responsible for carrying out the required action. This means that the sites end up having intimate knowledge of pretty much all the other classes in the program. The Visitor pattern helps eliminate this highly coupled state of affairs. (The pattern is described in refs [GoF] and [Vlissides]).
Using the Visitor pattern the site passes responsibility back to a third-party - a SiteManager say. The SiteManager can then perform the action required, probably via the aforementioned GameManager.
So the sequences of events becomes:
In this way Sites need only know about the SiteManager class. The SiteManager will know what actions need be called on from the GameManager on behalf of the particular type of Site. The SiteManager's Visit function is specific for different site types, so we have:
class SiteManager { public: SiteManager (); void Visit (GoSite&); void Visit (BuildingSite&); void Visit (CardSite&); // etc };
Each concrete Site class must implement the Accept function, e.g.
void CardSite::Accept (SiteManager& sm){ sm.Visit (*this); }
Up to this point I have been discussing ideas of how things might be done. This is very much how the original game was produced all those many years ago. An idea was settled on, which seemed to fit together, and the programming started.
It is design - of a sort, but the only rationale behind the choice of classes was that they seemed correct and that I was able to produce a working implementation of the game.
Some of the things I did not like about that design have been mentioned here, but this does not mean that the alternatives are better - just different.
References Cargill, Meyers, and Meyers2 discuss various aspects of these issues and put forward solutions for relatively simple examples to help illustrate their point.
With this Pet Project applying those solutions is not always straightforward because of other interactions required between the classes. However, the benefits of simply trying out the solutions are:
-
Better understanding of advantages and disadvantages of design solutions;
-
Ability to produce better and cleaner designs for new projects right from their start.
Unfortunately, and unlike this project, real-life time-scales and budgets often compromise some design decisions. But one is at least able to gauge the knock-on effects of any compromises made.
So we have done design (of a sort - as mentioned), but it has not been formal. The original implementation ended up with far too much coupling between the classes, and even some of the "solutions" discussed will not necessarily give the cleanest design and I would like to aim for the best and simplest design[5].
As you can see above, a lot of decision making has been left until the UML design is performed - as if UML is some kind of Utopia that will magically iron out all the problems discussed so far. I cannot say for certain if it will, but the purpose of The Pet Project is to be used as a real project to see how new languages, methodologies, techniques etc can be used both effectively and pragmatically.
Ref [Martin] gives a very interesting case study for the design of a Building Security Manager. I have started to use this same approach for Monopoly and am documenting each step and each decision being made during the design. I have tried to step back to square one and start the whole thing off from scratch, without any pre-conceived ideas on classes required and what their interactions will be. I will find it interesting to see what differences exist in the final design and implementation when compared to the dive-in approach I have taken so far.
Initially the UML Toolkit (ref [Eriksson-]) version of Rational Rose V4.0 was used, but its limitations in the number of use-cases and classes allowed were quickly reached. A couple of trial editions of other UML tools, UML-Magic and, very recently, WithClass[6]. To date I have yet to find one that gives me all the facilities I am after in such a design tool - so perhaps I'll have to write one - unless someone can recommend a better one.
I intend to submit my UML design to Overload when it is complete, but given the number of years I have being playing with C++ I guess you should not hold your breath - I might be playing with UML until I retire.
I noticed a footnote Francis made on the "Pet Project" article, regarding encapsulating the STL container in a class that provided the reference based interface. I had a think about this and initially thought this was an interesting idea. Then I thought it would be creating "yet another" list class. It would seem a shame to have a further list class when std::list provide all the functionality required, except for value based objects rather than reference based ones.
But, as a result of the comment, it then occurred to me that it would be useful to have a class whose sole responsibility is to make a value out of a reference. I came up with the following, which I believe achieves the purpose. I have quickly checked it out but still need to think about some of the common gotcha's. Also I am not aware if operator= is required in any of the STL container functionality, though I guess it would do no harm to provide it.
template<class T> class Ref { public: Ref (T& aT) : myT (&aT) { } Ref (Ref const& aRef) : myT (aRef.myT) { } ~Ref () { } Ref& operator= (Ref const& aRef) { myT = aRef.myT; return *this; } operator T&() { return *myT; } private: Ref (); private: T* myT; };
So now, if you have the following reference based class:
class Player // Ref based class { public: Player () { } ~Player () { } private: Player (Player const&); Player& operator= (Player const&); };
and the following:
typedef std::list<Ref<Player> > PlayerList;
It can be used as such:
{ Player p; PlayerList playerList; playerList.push_back (p); Player& pRef (playerList.back()); playerList.pop_back (); }
It also has the advantage that it can be used with any of the STL containers.
I keep asking myself if I have missed something - I'm sure there is a gotcha in there somewhere.
[GoF] Design Patterns - Elements of reusable software, Gamma et al; Addison Wesley; ISBN 0-201-63361-2
[Martin] Designing Object Oriented C++ Applications Using the Booch Method, Robert Martin; Prentice Hall; ISBN 0-13-203837-4
[1] So if anyone knows how to tie up some loose ends - well, I look forward to the article
[2] See part 2 for mention of how the Action method is used.
[3] In the case of the "Get out of Jail free" card
[4] From now on I'll just mention the Site class, but the same applies to Cards as well.
[5] However the metrics "best" and "simple" are measured!
[6] Comes with Borland C++ Builder Professional V5.0
Overload Journal #37 - May 2000 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> 37
(7)
All > Topics > Design (236) Any of these categories - All of these categories |