Among the design patterns it is likely one that impact the most the end-user: it lets you implement the undo and redo operations.
The template repository
CuisApp
14
demonstrates the design of a picture viewer, as a simple Cuis
application example. The user can rotate, flip and scale a picture
Its undo and redo actions are implemented with three classes:
undo
action – the
tail of the stack must be truncated with the truncate
method. A previous method is also needed for the undo
action.
All these methods share the same pattern: instantiate a command, then execute it:
flipHorizontally
|command| command := stack nextPut: (AppFlipHCommand presenter: presenter). ^ command execute
Some command requires additional parameters:
rotateLeft
|command| command := stack nextPut: (AppRotateCommand presenter: presenter). command degrees: -90. ^ command execute
For some action as flip horizontally, the unexecute is identical to execute:
AppFlipHCommand>>execute
self imageMorph image: (self imageMorph form flippedBy: #vertical)
AppFlipHCommand>>unexecute
self execute
Rotating requires an additional behavior (rotatedBy: not copied here) :
AppRotateCommand>>execute
self imageMorph image: (self rotatedBy: degrees)
AppRotateCommand>>unexecute
self imageMorph image: (self rotatedBy: degrees negated)
The execute and unexecute methods void there effects. In some circumstance, the execute may have a destructive effect as it happens in the zoom out action, an additional attribute is then used to keep a copy of the initial data:
AppZoomOutCommand>>execute
cacheForm := self imageMorph form. self imageMorph image: (cacheForm magnifyBy: 0.5)
So undoing restore properly the data:
AppZoomOutCommand>>unexecute
self imageMorph image: cacheForm
How execute and unexecute are implemented really depends on the nature of the command.
Figure 7.1: Command pattern diagram (for ease of reading the App prefix is removed)
What will be the implication to implement the undo and redo actions within our game? Our game is simple, the user can only click one card at a time, therefore we want to record this and also the consequences on the game state, if any. The consequences can be none, matching cards or non matching cards; each resulting in a different changed game state.
When implementing the user interaction with a command, the execute method will deal with these three possible outcomes. Its unexecute counterpart will have to reverse the game to its original state.
If the game state has a small memory footprint, it is less cumbersome to just save its state before executing each user action. The game state is the collection of each card’s status: the done and flipped Boolean values.
Our Memory game just need to be flanked with the command classes we described earlier, unchanged. Then the Command hierarchy will have one subclass PlayCardCommand:
Command subclass: #PlayCardCommand tanceVariableNames: 'status position' ...
It captures the game state in its status attribute, of the same nature as the cards array in the Memory game model:
initialize
status := Array2D newSize: presenter model cards size
At command execution,
execute
self backupModels. presenter flip: position
the game state is backed up before flipping the card:
backupModels
| size | size := presenter model cards size. 1 to: size y do: [:y | 1 to: size x do: [:x | | card | card := presenter model cards at: x@y. status at: x@y put: (Array with: card isFlipped with: card isDone) ]]
The undo action restores the game state before execution:
unexecute
" Restore the status of the card models " | size | size := status size. 1 to: size y do: [:y | 1 to: size x do: [:x | | cardStatus card | card := presenter model cards at: x@y. cardStatus := status at: x@y. card flip: cardStatus first; done: cardStatus second ] ]
The card models’s flip: and done: methods are refactored to trigger events propagated to the card view:
MemoryCardModel>>flip: boolean
" Set my flip state and trigger a color event for my view accordingly to my flip state " | newColor | flipped = boolean ifTrue: [^ self]. flipped := boolean. newColor := flipped ifTrue: [color] ifFalse: [self backColor]. self triggerEvent: #color with: newColor
and
done: boolean
done = boolean ifTrue: [^ self]. done := boolean. self triggerEvent: (done ifTrue: [#lock] ifFalse: [#unlock])
In Memory Game v3, you will find the complete source of the modified Memory game: toolbar with undo and redo buttons.