MVC8 and MVP9 designs are very close. MVC, nowadays widely used in GUI and Web application developments, was invented by the Smalltalk community in the late seventies and early eighties. MVP is a subtle variation where the presenter has more responsibilities than the controller and acts more as a middle-man between the model and view objects.
Let’s review the responsibilities of each three objects.
The model knows nothing about the view and presenter involved in the design. However, a model modifying itself its attributes should have a way to notify the views to update their representation of the model, it is the role of the dependency mechanism explained later in the chapter.
The presenter also handles the user actions mediated by the views of a given model. Therefore, when the user edit a text entry, click on a button, select an entry in a menu or drag a visual object, the event is handled to the presenter. Then it decides, depending on the context and the state of the application, what to do with the event, like updating the state of the application and handling suited data to the model eventually.
Now, let’s see how to reshape our memory game to fit it in the MVP design.
The model is the isolated object, without knowledge of the presenter and the view11, it is easier to start from there.
In the previous design of the game, we had two classes MemoryGameWindow and MemoryCard acting as view and model. Therefore we need to extract what is model related.
Our game involves the domain of a game with cards, we define two models:
Object subclass: #MemoryCardModel instanceVariableNames: 'flipped done color' ...
We add the necessary initialization and methods to externally get the state and to update it.
initialize
done := flipped := false
When a card has been successfully associated with cards sharing the same colour, we set it as done and it can’t be played anymore
setDone
done := true
To evaluate the available card to play, we need to know if a card is done or not
isDone
^ done
During a player move, we need to know if a card is already flipped, if so the play can’t flip it again
isFlipped
^ flipped
Object subclass: #MemoryGameModel instanceVariableNames: 'size tupleSize cards' ...
We have the necessary methods to initialize the instance and to create the card models. Compared to its counterpart MemoryGameWindow>>installCards of the previous design, the method installCards here after is much simpler and easier to understand because it only instantiate the card models. Separation of responsibility is paying off:
initialize
size := 4 @ 3. tupleSize := 2
installCardModels
| colours | cards := Array2D newSize: size. colours := self distributeColors. 1 to: size y do: [:y | 1 to: size x do: [:x | cards at: x@y put: (MemoryCardModel new color: colours removeFirst) ] ]
In this class, we also import, unchanged, the behaviors of MemoryGameWindow fitting the game model: distributeColors, doneCards, flippedCards, undoneCards and isGameWon.
That’s it for the models of the game.
We have defined two model classes, so we may expect to define two view classes. Well, not necessary, here we just need to define one view class for the whole game and we use an existing view of Cuis-Smalltalk for the card model, the PluggableButtonMorph
We need to reshape MemoryGameWindow to contain only view related business, first in its attributes then its behaviors. First at all, a view always knows about its presenter, it can even know about the model through the mediation of the presenter
presenter: aPresenter
presenter := aPresenter. self model: presenter model
It also knows about some other views needed for its internal organisation and regulation
SystemWindow subclass: #MemoryGameWindow instanceVariableNames: 'presenter statusBar playground' ...
Again the behaviors is stripped down to only view considerations and the initialize method is shortened.
Installing the toolbar slightly differs,
installToolbar
| toolbar button | toolbar := LayoutMorph newRow separation: 2. button := PluggableButtonMorph model: presenter action: #startGame :: enableSelector: #isStopped; ...
the model of the buttons is not any more the view but the presenter.
Indeed, we explained earlier it is the presenter responsibility to handle user events, the actions remain the same and we can anticipate the related methods will be transferred from the view to the presenter class.
Now, we should look at installing the card views,
installCards
| row size | playground removeAllMorphs. size := model size. 1 to: size y do: [:y | row := LayoutMorph newRow. 1 to: size x do: [:x | | cardModel cardView | cardModel := model cards at: x@y. cardView := PluggableButtonMorph model: presenter action: #flip: actionArgument: x@y. ... cardView layoutSpec proportionalWidth: 1; proportionalHeight: 1. cardView color: cardModel backColor. row addMorph: cardView]. playground addMorph: row ]
it relies on the already instantiated card models; we ask the game model all the card models: model cards.
Observe how we just use a stock PluggableButtonMorph as a view for the card, indeed we don’t need to specialize its behavior, so we keep it simple. Again, the presenter handles the user click on the card, it should be understood as executing the statement presenter flip: x@y at the event12
For clarity, we have presented above a shortened version of the installCards method, without the dependencies between the card models and the card views. The logic of installing the card models then the card views is handled by the presenter, the middle-man, We discuss it in the next section.
We define a new class MemoryGame as our presenter,
Object subclass: #MemoryGame instanceVariableNames: 'model view playing' classVariableNames: '' poolDictionaries: '' category: 'MemoryGameV2'
It acts as the entry point of a GUI application, therefore its name is kept short with no Presenter fragment. A new game instance is then invoked by a simple
MemoryGame new
As we explained earlier, the presenter instantiates both the model and the view,
initialize
model := MemoryGameModel new. view := MemoryGameWindow presenter: self. self startGame. view openInWorld
observe the game model is attached neither to the view nor to the presenter.
From the initialization, the game then is started
startGame
model installCardModels. view installCards. view message: 'Starting a new game' bold green. view setLabel: 'P L A Y I N G'. playing := true
by invoking card models and views installations, it is asked to each object in charge of that business.
We already learn the flip: method, called when the user click on a card, is now defined in the presenter.
The method is quite similar to the previous iteration, except now we only know about the card model, the associated card view is unknown
flip: position
| flippedCards | (model cards at: position) flip; triggerEvent: #lock. flippedCards := model flippedCards. ... " Unflip and unlock the flipped cards " flippedCards do: [:aCard | aCard flip; triggerEvent: #flash; triggerEvent: #unlock]. ^ self]. ... " We found a n-tuple! " view message: 'Great!' bold, ' You find a ', model tupleSize asString, '-tuple!'. flippedCards do: [:aCard | aCard triggerEvent: #flash]. flippedCards do: #setDone. ...
Therefore, to update the state of a card view, a card model triggers events which are propagated to any card view listening to the events. An event is coded as a symbol representing an aspect of the model that changed. The symbol name is arbitrary chosen to be meaningful. In the flip: method there are three events:
When triggering an event, it is additionally possible to pass along a parameter. Observe this feature in the model’s flip method to inform about the card color changed
MemoryCardModel>>flip
| newColor | flipped := flipped not. newColor := flipped ifTrue: [color ] ifFalse: [self backColor]. self triggerEvent: #color with: newColor
All in all, there are four events triggered by a card model: lock, flash, unlock and color. How a view can listen to a given event is discussed in the next section.
https://en.wikipedia.org/wiki/Model-view-controller
https://en.wikipedia.org/wiki/Model-view-presenter
This is an assumed variation from the view used as the entry point of the traditional approach. Being a middle-man, it makes sense it is instantiated first.
Though several models may know about each other.
The message #model:action:actionArgument: to instantiate a button is extremely confusing in its model: keyword; it is not a model as we discussed earlier but only the receiver of the user action, the controller in the sense of the MVC pattern.