I'll make a puzzle program to illustrate the essence of the MVC paradigm. EngiPuzzle is in Appendix 22. The program is intended to show clearly how to use MVC effectively.
File in Appendix 22 and evaluate Program 37-1.
Program-37-1: (EngiPuzzleModel, EngiPuzzleView) ------------------------------------------------- | puzzleModel | puzzleModel := EngiPuzzleModel new. EngiPuzzleView openOn: puzzleModel -------------------------------------------------
You'll see the well-know "15's puzzle." If you solve the puzzle, you'll hear 3 beeps. Use the yellow-button menu to randomize or order the buttons.
Next, I'd like show you how to operate the model with messages. Program 37-2 opens two puzzle windows and an inspector on the same puzzle model.
Program-37-2: (EngiPuzzleView, EngiPuzzleModel) ------------------------------------------------- | puzzleModel | puzzleModel := EngiPuzzleModel new. EngiPuzzleView openOn: puzzleModel. EngiPuzzleView openOn: puzzleModel. puzzleModel inspect -------------------------------------------------
Evaluate Program 37-3 in the inspector (right view). The 'self' in the program is the puzzle model itself.
Program-37-3: (Delay, EngiPuzzleModel) ------------------------------------------------- self normalize. (Delay forSeconds: 1) wait. self moveStone: 3 @ 4. (Delay forSeconds: 1) wait. self moveStone: 3 @ 3. (Delay forSeconds: 1) wait. self moveStone: (self whereIsBlank - (2 @ 0)). (Delay forSeconds: 1) wait. self moveStone: (self whereIsBlank - (0 @ 2)). (Delay forSeconds: 1) wait. self moveStone: (self whereIsBlank + (0 @ 2)). (Delay forSeconds: 1) wait. self moveStone: (self whereIsBlank + (2 @ 0)). (Delay forSeconds: 1) wait. self moveStone: 3 @ 4. (Delay forSeconds: 1) wait. self moveStone: 4 @ 4 -------------------------------------------------
You should see the pieces in both windows line up, then the blank will move about once a second for several moves, then return to where it started, at which point you'll hear 3 beeps.
We have studied MVC a little, and some of you probably have a pretty good understanding. However, I'd like to review a little.
Program 37-4 accomplishes the same as Program 37-3, but without a user interface, so you can not see anything happening. If you execute the program you'll see the mouse cursor change to execution mode (an arrow with a star) for about 8 seconds and then you'll hear 3 beeps.
Program-37-4: (Delay, EngiPuzzleModel) ------------------------------------------------- | puzzleModel | puzzleModel := EngiPuzzleModel new. puzzleModel normalize. (Delay forSeconds: 1) wait. puzzleModel moveStone: 3 @ 4. (Delay forSeconds: 1) wait. puzzleModel moveStone: 3 @ 3. (Delay forSeconds: 1) wait. puzzleModel moveStone: (puzzleModel whereIsBlank - (2 @ 0)). (Delay forSeconds: 1) wait. puzzleModel moveStone: (puzzleModel whereIsBlank - (0 @ 2)). (Delay forSeconds: 1) wait. puzzleModel moveStone: (puzzleModel whereIsBlank + (0 @ 2)). (Delay forSeconds: 1) wait. puzzleModel moveStone: (puzzleModel whereIsBlank + (2 @ 0)). (Delay forSeconds: 1) wait. puzzleModel moveStone: 3 @ 4. (Delay forSeconds: 1) wait. puzzleModel moveStone: 4 @ 4. ^puzzleModel -------------------------------------------------
What I want to remind you of now is that you can operate the model without the view or the controller by sending messages directly to the model.
The model is more important than the view or the controller. Since there are many good user-interface builders, it is tempting to forget that the design of the model should come before the design of the interface.
You must give sufficient consideration to the analysis and design of the model, because it is a representation of reality, a virtual reality. The view and controller just work to visualize that reality. So the fact that user-interfaces are now easier to build than ever should not tempt you to start there, but to more thouroughly analyze the problem, knowing that building the interface will take less time.
Good Smalltalkers start with the model and confirm that it (virtual reality) works well alone before proceeding to add the view and controller. UI builders help to complete this process. So this is the process of designing good MVC systems. Furthermore, making the MVC pluggable is even better, because it provides weak coupling with the minimum of messages. So strive for this kind of design.
By default, the example puzzle size is 4 by 4, but you can specify other row and column sizes as in Program 37-5.
Program-37-5: (EngiPuzzleModel, EngiPuzzleView) ------------------------------------------------- | puzzleModel | puzzleModel := EngiPuzzleModel grid: 5 @ 6. EngiPuzzleView openOn: puzzleModel -------------------------------------------------
Also, you can change the image the puzzle. Program 37-6 prompts the user for any rectangle on the screen and uses it for the puzzle surface. This can make the puzzle quite a bit harder...
Program-37-6: (EngiPuzzleModel, EngiPuzzleView, Image) ------------------------------------------------- | puzzleModel | puzzleModel := EngiPuzzleModel grid: 4 @ 3 image: Image fromUser. EngiPuzzleView openOn: puzzleModel -------------------------------------------------
Let's examine the puzzle program in detail. The inheritance trees are:
------------------------------------------------- Object () . Model ('dependents') . . EngiPuzzleModel ('puzzleGrid' 'puzzleImage' 'puzzleBoard') ------------------------------------------------- Object () . VisualComponent () . . VisualPart ('container') . . . DependentPart ('model') . . . . View ('controller') . . . . . EngiPuzzleView () ------------------------------------------------- Object () . Controller ('model' 'view' 'sensor') . . ControllerWithMenu ('menuHolder' 'performer') . . . EngiPuzzleController () -------------------------------------------------
The model has three instance variables: 'puzzleGrid', 'puzzleImage', and 'puzzleBoard'. 'puzzleGrid' conatins the matrix dimensions. 'puzzleImage' stores the image of the puzzle. 'puzzleBoard' is an array of rows, each of which contains the index of the piece at the given location. Pieces are numbered sequentially from 1, and they are moved in the two dimensional array according to your play. Then empty space could be represented by any unique identifier; here it is just the highest number, or rows * columns.
You can get the location of the empty piece with the 'whereIsBlank' message. 'isPerfect' returns whether the puzzle is solved. 'moveStone:' moves the given piece. Of course if you specify a piece which can not move, it has no effect.
When the model changes, a message is broadcast. For example 'normalize' to line up pieces, 'randomize' to disorder, or 'moveStone:to:' which is a subordinate message of 'moveStone:'.
The view just draws the puzzle board based on the stored image of pieces (puzzleImage). The view's 'update' method redraws the whole view for 'normalize' and 'randomize'. But it redraws the minimal part of the view for 'moveStone:'.
The controller carries out message sending for the human player. It just translates the mouse click location to a row and column coordinate and tells the model to move a piece based on that point. It also provides a pop up menu to 'normalize' or 'randomize'.
Now that the basic model, view and controller are defined, why don't you try to add code to solve the puzzle programmatically? Just remember the points about good MVC design.