ObserverOur example this time is a card playing game that also shows statistics to test the newly created AI (this is not unrealistic, I have made such a thing for robot football). The results shall be shown during the game by several windows containing charts and tables of different types and data. The statistics can also be turned off.
points display
^
|
|
card table <-- state --> chart A
|
|
v
chart B
This means everytime the state of the game has changed, there are lots of objects that have to be updated. In our example this is the GUI which shows the card table itself, so the player sees what is going on. But there are also several windows with statistics and a display showing the points a player got. The state of the game changes either because a human player made a move or the AI.
The first tryThe first approach that comes into your mind may be this (pseudocode):
class PointsDisplay {
attribute GameState gameState;
method testForUpdate() {
every second do {
if(gameState.hasStateChanged()){
displayPoints(gameState.getPoints());
}
}
}
}
ChartA, ChartB and CardTable do something similar.
This is pretty bad for performance, because a lot of classes do needless calles to check whether the state has changed.
This goes hand in hand with synchronization issues.
The second tryThis time we do it the other way around. The object that changes the state has to call the update methods of every view component.
class AI {
method nextMove(){
Card card = takeCard();
updateChartA(card);
updateChartB(card);
updateCardTable(card);
updatePointsDisplay(card);
}
}
class CardTable { //This is the gui that shows data, but also
//retrieves input of the human player
method playersHasChosenCard(Card card){
updateChartA(card);
updateChartB(card);
updateCardTable(card);
updatePointsDisplay(card);
}
}
Now everytime we add a new way to change the data (i.e. another AI), we also need to add a huge list of method calls that update everything. And if there is a new chart or table that has to be updated, we have to add an update for it everywhere in the program. The problem is that the updates are called in different locations of the code. Forgetting one location is likely. It is a better approach to have this sorted somehow.
The third trySince the last solution is not appropriate we try a new one. Instead of updating everything in different locations we put it entirely into the GameState itself. This means we need an instance of every object that has to be updated.
class GameState {
attribute ChartA chartA;
attribute ChartB chartB;
attribute PointsDisplay points;
attribute CardTable table;
method update(){
chartA.update(updateInfo);
chartB.update(updateInfo);
points.update(updateInfo);
table.update(updateInfo);
}
}
This is much better. There is only one location we need to change when we add a new chart. But it is still not perfect. The list of attributes (and thus also update-calls) may grow a lot. Also we said that we wanted to show only the statistics the user chose to. There is no need in updating charts that are not available right now. We would have to do something like this:
class GameState {
attribute ChartA chartA;
attribute ChartB chartB;
attribute PointsDisplay points;
attribute CardTable table;
method update(){
if(chartA.isAvailable()) {
chartA.update(updateInfo);
}
if(chartB.isAvailable()) {
chartB.update(updateInfo);
}
points.update(updateInfo);
table.update(updateInfo);
}
}
This is where the observer pattern comes into play.
The Observer patternThis is the UML diagram for the Observer pattern:
Now the GameState notifys all observers when its data has changed. You can imagine this like setting the option to get an e-mail-notification everytime someone has send you a PM on EZ. When you see the e-mail you will know that you have to update yourself by logging into the forum and reading the PM.
To notify every Observer the GameState has a collection of them. The notify()-method will look like this:
class GameState {
attribute ObserverCollection observers;
method notify(){
foreach Observer o in observers {
o.update();
}
}
}
The update()-method of an Observer could be implemented like this:
class PointsDisplay extends Observer {
attribute GameState gameState;
method update() {
State state = gameState.getState();
displayPoints(state.getPoints());
}
}
If the user doesn't want to show some charts, the removeObserver-method() is called. We have no unnessecary if-statements anymore to check whether an update is required. The code is reduced to a minimum and easily extendable.
Design principle 3: Aim at a loosely coupled design for interacting objects.
Deque