Another common scenario is an application that would like to present many different look-and-feels. Some users may want a tool bar and keyboard accelerators; while other users may prefer a menu bar and pop-up menus. Along a different dimension, some users may prefer to browse complex data in tables; while other users may prefer graphs and bar charts.
These kinds of "separation of concerns" are so common in software that several reusable architectural approaches have been documented and practiced over the years. The Smalltalk community instituted MVC - Model, View, Controller. Encapsulate the independent functionality in a common core - the Model. Segregate the dependent functionality into as many Views as the requirements call for. Then choreograph all interaction between the two camps by interposing a Controller.
Another manifestation of this same paradigm is the Observer design pattern [footnote]. It prescribes that the sentience, or intelligence, of the application is partitioned in a component called the Subject. Then all optional, and often user-configurable, functionality is captured in as many Observers as there are flavors to the user experience.
Interface and Implementation
Whenever two parties are involved in anything (whether its a business, a marriage, or a computer application), a contract or an "understanding" is involved. The reasons for complicating life by bringing together and choreographing the energies and expectations of two parties might be:
Object-oriented developers are even more jealous about distinguishing between interface and implementation. UML defines an element called "interface". Java defines a construct called "interface". The motivation of each is to define a contract between two or more parties that carefully structures the separation of concerns that needs to be enumerated and enforced.
In our context the parties are: the model, and one or more views. How should development proceed? Should the interface, the model, and one or more views be specified sequentially or simultaneously? If sequentially, which should be developed first?
It is a common practice to design and implement the model, the interface, and one view simultaneously. Then when the second and third views don't mesh well with the interface, the entire project scrambles to recover. Robert Martin wrote an article that documented just such an experience.
Reuse case study
Martin's company was contracted to write two vocational testing applications. If the first two deliverables were successful, then eleven more testing applications would be awarded. Their architecture called for a framework that would be the focus and repository of all reuse. Given the similarity of the applications, they estimated each application would consist of: 85% reused code from the framework, and 15% application-specific code. They decided that reuse would be maximized if the framework and the first application were developed simultaneously.
When the second application was started, Martin was dismayed to learn that the framework wasn't very reusable. This was because proactive reuse is almost always an attempt to exercise 20/20 foresight - something humans are not well-equipped to practice. Grady Booch has suggested, "a framework does not even begin to reach maturity until it has been applied in at least three or more distinct applications."
Martin and company decided to re-develop the framework concurrently with the development of four client applications. "We did not trust our ability to completely anticipate the needs of the [applications]. In short, we felt that the architecture needed almost immediate verification by being used in working [applications]. We did not want to guess."
The purpose for including this reuse tangent is to suggest that an application's model and view interface need to be developed in parallel with three or more views. We won't invest in that level of discipline. We will focus on just two views: a DOS view, and a GUI view.
Two iterations:
The Observer design pattern recommends centralizing the state of the application in the Subject (aka Model). Whenever changes occur to that data, the Subject communicates with all Observers (aka Views) present. The designer can choose to have the Subject "push" these changes to each Observer, or, the Observers can each "pull" whatever state they desire from the Subject. The first choice increases the coupling between Subject and Observer because the Subject has to know (or assume) what each Observer wants. The second choice incurs more overhead because the Subject sends a message to all Observer, which in turn send a request back to the Subject.
What state exists in our current application? The current location of each square?
A common compromise in Model-View designs is to replicate some amount of the application's state in each View. If there is no replication at all, then each View must go back to the Model, and request all the application data is it responsible for presenting each and every time it needs to refresh its presentation. This replication could be thought of as "caching" - a common technique for improving performance.
Do you want (or need) to cache any application state in either view? Instructions and issues:
Listing 3.1
import java.awt.*; import java.awt.event.*; import javax.swing.*; public class FifteenPuzzleSwing implements ActionListener { public FifteenPuzzleSwing( int w, int h ) { JFrame frame = new JFrame( "Fifteen Puzzle" ); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.getContentPane().setLayout( new GridLayout( h, w ) ); Font font = new Font( "Helvetica", Font.BOLD, 24 ); JButton button; for (int i=0; i < w * h; i++) { button = new JButton( "" + (i+1) ); button.setFont( font ); button.setBackground( Color.white ); button.addActionListener( this ); frame.getContentPane().add( button ); } frame.pack(); frame.setVisible( true ); } public void actionPerformed( ActionEvent e) { String str = ((JButton)e.getSource()).getText(); System.out.println( str + " pressed" ); } public static void main( String[] args ) { System.out.print( "Width: " ); int w = Integer.parseInt( IOutils.getKeyboard() ); System.out.print( "Height: " ); int h = Integer.parseInt( IOutils.getKeyboard() ); FifteenPuzzleSwing view = new FifteenPuzzleSwing( w, h ); } }
The only coupling that needs to exist between the Model and its corresponding View is a setLabel() method. When the Model is told to move(), it works through the algorithm we created in the previous chapter, and when swap() is required, the View is updated at the same time as the local state.
Notice that the FifteenPuzzleModel class is dependent only on the FifteenPuzzleView interface. The Model receives the View object in the constructor, and delegates to it in the swap() method.
Listing 3.2
interface FifteenPuzzleView { void setLabel( int position, int value ); } public class FifteenPuzzleModel { ... private FifteenPuzzleView view; public FifteenPuzzleModel( int w, int h, FifteenPuzzleView v ) { view = v; ... } public void move( int number ) { ... } private boolean tryAbove( int pos ) { ... } private boolean tryBelow( int pos ) { ... } private boolean tryLeft( int pos ) { ... } private boolean tryRight( int pos ) { ... } private void swap( int one, int two ) { ... view.setLabel( one, squares[one] ); view.setLabel( two, squares[two] ); } }It would seem ideal if the FifteenPuzzleSwing class could be reduced to the actionPerformed() method. This is generally the case in listing 3.3. All presentation functionality (component layout and exposure recovery) is handled for us by the Swing library. All that remains to be handled by us is user interaction.
The FifteenPuzzleSwing class implements the previously defined FifteenPuzzleView interface. This allows the coupling that exists from the Model to the View to be abstract (or indirect).
When the user selects a JButton component, our actionPerformed() method is invoked. If the user has selected the blank space, then no action is required. Otherwise, the JButton's label is converted to an integer and passed to the Model's move() method. If the Model decides the move request is valid, it will update the View by calling setLabel(), which in turn modifies the label of the affected JButton.
Notice that this design not only has the Model updating the View, but the View initiating action on the Model. For this to work, each must hold a reference to the other. This "chicken and egg" cyclic coupling was resolved by passing the View reference to the Model when the latter is created, and then passing the Model reference to the View in a subsequent message.
Listing 3.3
public class FifteenPuzzleSwing implements FifteenPuzzleView, ActionListener { private JButton[] buttons; private FifteenPuzzleModel model; public FifteenPuzzleSwing( int w, int h ) { ... } public void setModel( FifteenPuzzleModel m ) { model = m; } public void setLabel( int position, int value ) { buttons[position].setText( (value == 0 ? "" : "" + value) ); } public void actionPerformed( ActionEvent e) { String str = ((JButton)e.getSource()).getText(); if (str.equals( "" )) return; model.move( Integer.parseInt( str ) ); } public static void main( String[] args ) { ... FifteenPuzzleSwing view = new FifteenPuzzleSwing( w, h ); FifteenPuzzleModel model = new FifteenPuzzleModel( w, h, view ); view.setModel( model ); } }It would seem ideal if the FifteenPuzzleDos class could be reduced to the display() method and the interaction loop. This is demonstrated in listing 3.4. The only interesting difference between this View and the Swing View is that we are caching the width, height, and square label state here. This allows the performance of the display() method to be improved.
Listing 3.4
public class FifteenPuzzleDos implements FifteenPuzzleView { private int width, height; private int[] squares; public FifteenPuzzleDos( int w, int h ) { ... } public void setLabel( int position, int value ) { squares[position] = value; } public void display() { ... } public void start( FifteenPuzzleModel model ) { int number; while (true) { display(); System.out.print( "\nMove: " ); number = Integer.parseInt( IOutils.getKeyboard() ); if (number == 0) break; model.move( number ); } } public static void main( String[] args ) { ... FifteenPuzzleDos view = new FifteenPuzzleDos( w, h ); FifteenPuzzleModel model = new FifteenPuzzleModel( w, h, view ); view.start( model ); } }