What do I mean? A Top Trump game consists of you looking at your card and choosing what you believe the be the best statistic on your card and challanging your opponent. This leads us to expose the data of our Top Trump object using properties so we can see them clearly. In reality however the Top Trump card should be keeping it's data secret: it is only exposing it's data to you not to your opponent, you merle ask your opponent if your card beats his but you never know what data his card contains.
If we were to model this in an OO world we would probably have a TopTrump class with various comparison methods. We wouldn't want to do the comparisons ourself. As an example we wouldn't do:
public void ChallangeOnSpeed()Instead we'd have the Trumps make their own desicions:
{
if(me.CurrentTrump.Speed > opponent.CurrentTrump.Speed)
{
me.TakeTrumpFrom(opponent);
}
else if(me.CurrentTrump.Speed < opponent.CurrentTrump.Speed)
{
me.GiveUpTrumpTo(opponent);
}
else
{
// it's a draw
}
}
public void Challange(IStatistic statistic)So what has this gained us? It is no longer down to the player to compare the information the trumps expose instead the responsibility for comparison is with the trumps themselves. Now the trumps are autonomous in deciding how they should behave: they are encapsulated. "And the point of that was?" To answer that question let's look at the evolution of our model. The first version of our software only supported one type of Trump and that was the Supercars pack. Our players could compare directly by choosing ChallangeOnSpeed etc. but now we've designed a new Marvel Comic Heroes pack which have different categories. We'd have to either add new methods or create new Player classes to handle the new Trump classes and new Game classes to handle the new Player classes. What a load of work! Then what happens when Dinosaurs pack or the Horror pack comes out? Or what if we go global and the speed is worked out in KM/H as well as MPH or dollars as well as sterling? By exposing the data we have overburdened the Player and the Game classes with responsibility causing a maintanance nightmare so now everytime our domain evolves our code breaks.
{
ChallangeResult challangeResult = myTrump.ChallangeWith(statistic);
if(challangeResult.Equals(Won))
{
TakeTrumpFrom(opponent);
}
else if(challangeResult.Equals(Lost))
{
GiveUpTrumpTo(opponent);
}
else
{
// it's a draw
}
}
This is why properties break encapsulation when they expose data directly. When we put the responsibility to compare data on the class which doesn't own it then any change to the class that own's the data has a ripple effect through your whole code breaking Single Responsibility Principle. If on the other hand the object itself is responsible for carrying out the operations on it's data you only make one change and in turn you strengthen the concepts of your domain.
"OK" I hear you say "but you have to expose properties so you can render the data on the user interface". Ah but do you? What if the Trump object told the UI what to display? "Surely that would mean putting UI code into the Trump object and that's just wrong and anyway I need that data to save to the database as well does that mean we also end up with data code in the class that's even worse?" Not if we use the Mediator pattern (hey imagine a Design Patterns Top Trumps!).
Mediator to the rescue
On a side note I'd like to discuss why I said Mediator over Builder. Some people use the Builder pattern (Dave Astels did in his One Expectation Per Test example) which is a construction pattern used to seperate the construction of an object from it's representation. The purpose of the Builder is that at the end you get an object. However when working with a UI you may not have an object as an end product instead you may be just sticking your values into existing objects. The Mediator on the other hand merle acts as an interface which defines the interaction between objects to maintain loose coupling. You could argue that some implementations of Builder are really Mediator with a Factory method (or a combination of both as we shall soon see).
Well let's start by defining our mediator for our Top Trump. For those who have read my blog before you'd know I'm a big fan of Behaviour/Test Driven Development but for the sake of consisnese I'll deal only with the implementations for these examples.
public class TopTrumpOf course you could use a property (what?) for setting the data if you so wished (I think that's perfectly legitamate as it is a behaviour to change the value) however there are good arguments for using a method one of them being overloading (suppose we wanted SetSpeed(string)).
{
int speed;
public void Mediate(ITopTrumpMediator mediator)
{
mediator.SetSpeed(speed);
}
}
public interface ITopTrumpMediator
{
void SetSpeed(int speed);
}
Now when we implement our concrete Mediator we tie it to the view:
public class UiTopTrumpMediator : ITopTrumpMediatorThat works nicely and of course you could implement a database Mediator as well using one method regardless of the destination (I love OO don't you?). The only thing is our Mediator is a bit too close to the exact representation of the object. If we were to introduce our new packs we'd have to rewrite our mediator interface and all the classes that consume it. What we need to do is get back to our domain concepts and start dealing in those again:
{
private readonly ITopTrumView view;
public UiTopTrumpMediator(ITopTrumView view)
{
this.view = view;
}
public void SetSpeed(int speed)
{
view.SpeedTextBox.Speed = speed.ToString();
}
}
public interface ITopTrump
{
void Mediate(ITopTrumpMediator mediator);
}
public interface ITopTrumpMediator
{
void AddStatistic(IStatistic);
}
public class SupercarTrump : ITopTrump
{
int speed;
SpeedUnit speedUnit;
public void Mediate(ITopTrumpMediator mediator)Then on IStatistic we'd add a ToString method like so:
{
mediator.AddStatistic(new SpeedStatistic(speed, speedUnit));
}
}
public class DinosaursTrump : ITopTrump
{
StrengthStatistic strength;
public void Mediate(ITopTrumpMediator mediator)
{
mediator.AddStatistic(strength);
}
}
public interface IStatisticOf course we could go on and on refining and refactoring - adding muli-lingual support to our mediator etc. - but hopefully you get the picture; by encapsulating the data of the object and placing the stress on it's behaviour we protect it's internal representation from being exposed thus decreasing it's coupling and making it more flexible and stable during change.
{
string ToString();
}
public class SpeedStatistic
{
public string ToString()
{
return String.Format("{0}{1}", speed, speedUnit);
}
}
Now if I'm honest with you after having all the above revelations there was one scenario I struggled with which I hadn't found any good examples to. Basically everyone talks about displaying data to the UI but never about changing the object from the UI. Changing the data implies breaking encapsulation in someway and if the UI isn't allowed to know about the internal representation how is it supposed to change it? Basically how would our Trump Designer package create new Trump cards and edit existing ones?
Well creation is easy: we'd use the builder pattern and have a concrete implementation of ITopTrumpBuilder for each type of Top Trump card. The UI would then simply engage the ITopTrumpBuilder and pass it's data across in much the same fashion as with the mediator just in reverse. The builder could even tell us if the resulting Trump is valid before we try and get the product.
Remember memento? (not the film the pattern)
But still what about editing an object? There's a pattern called Memento which because of the film is probably the catchiest of all the pattern names but it still is quite a rarity to see it used. That's because Memento's core purpose is for undo behaviour which is very rare on enterprise systems but it is handy for general editing scenarios. Basically Memento is either a nested private class which either holds or is allowed to manipulate the state it's container (the originator) or an internal class which the originator loads and extracts it's values from. Therefore Mementos offer a very nice way of encapsulating edit behaviour if we combine it with Mediator to create a public interface which objects external to the domain can use.
public interface ITopTrumpMementoSo there you go: now your UI (or database or webservice) can work with the ITopTrumpMemento interface for editing ITopTrump objects and you can add new TopTrump classes which store their internal data with varying different methods to your hearts content without every breaking any code!
{
void UpdateStatistic(IStatistic);
}
public class SupercarTrump : ITopTrump
{
private State state;
private class State
{
internal int Speed;
internal SpeedUnit speedUnit;
}
private class SupercarTrumpMemento : ITopTrumpMemento
{
private State state;
private SupercarTrumpMemento(State state)
{
this.state = state;
}
private State GetState()
{
return state;
}
public void UpdateStatistic(IStatistic statistic)
{
speedStatistic = statistic as SpeedStatistic;
if(speedStatistic != null)
{
state.Speed = speedStatistic.Speed;
}
}
}
public ITopTrumpMemento CreateMemento()
{
return new SupercarTrumpMemento(state);
}
public void Update(ITopTrumpMemento memento)
{
this.state = memento.GetState();
}
}
There are advantages to this too numerous to mention; loose coupling is promoted as the UI never gets near the domain, testing is made far easier as you can use mocks of the IMediator, IBuilder and IMemento instead of working with the domain objects direct and also reusability is increased as the mediators take the responsibility away from your presenters.
Tip:
The trick with maintaining your encapsulation as neatly as possible is to try and ensure that your IMediators, IBuilders and IMementos all deal with the concepts of their domain (for example IStatistic) and not the structure of the data (e.g. int speed).
No comments:
Post a Comment