MVC8 and MVP9 designs are very similar. MVC, nowadays widely used in GUI and Web application development, 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 middleman between the model and view objects.
Let’s review the responsibilities of each of the three objects.
The model knows nothing about the view and presenter involved in the design. However, a model that modifies its own attributes should have a way to notify the views to update their representation of the model. This is the role of the dependency mechanism explained later in the chapter.
The presenter also handles user actions mediated by the views of a given model. Therefore, when the user edits a text entry, clicks on a button, selects an entry in a menu, or drags a visual object, the event is handled by 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 into the MVP design.
The model is the isolated object, without knowledge of the presenter and the view11, so 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 cards to play, we need to know if a card is done or not:
isDone
^ done
During a player’s move, we need to know if a card is already flipped. If so, the player 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 is much simpler and easier to understand because it only instantiates 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 might expect to define two view classes. Well, not necessarily. 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 of 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 behavior 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 anymore the view but the presenter.
Indeed, we explained earlier that it is the presenter’s 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 for 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’s click on the card. This 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 middleman. 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 that the game model is attached neither to the view nor to the presenter.
From the initialization, the game is then 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, each object in charge of that business is asked to perform its task.
We already learned the flip: method, called when the user clicks 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 an n-tuple! " view message: 'Great!' bold, ' You found 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 has changed. The symbol name is arbitrarily 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, which is used as the entry point of the traditional approach. Being a middleman, it makes sense that 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.