Last year, my 11-year-old son Tim expressed an interest in learning how to program. With visions of StarCraft in his head, he wanted to tackle building a game. I suspect that the compromises we madefirst trying the game of dots and finally settling on a number-guessing gamewere a disappointment to him. They were to me, too, but I've learned to become just a bit realistic over the years.
We sat down and briefly discussed programming, talking about computers, programs, classes, objects and messages. Tim took all this in and asked some good questions. We decided that the game of dots would be simpler than a full-blown shoot-'em-up adventure game. So far, so good.
We then talked a bit more, about languages and compilers. Tim knew that there was a language called Java, and that Dad knew something about it. I talked a bit about other possible choices, including Smalltalk and Ruby. No diceTim wanted to jump on the bandwagon, so we settled on Java. That was mistake number two.
Our first mistake was choosing the game of dots.
Game Glitches
Many of you will recall the pencil-and-paper game of dots from childhood. A grid of dots is drawn. On each turn, a player draws a horizontal or vertical line to connect a pair of dots. If a player draws a line that closes a box, he fills the box with his initials. The player continues to draw lines as long as a box is closed each time. The player with the most initials when the entire grid is filled wins.
On the surface, this is a reasonably simple game. From a programming perspective, it requires some knowledge of drawing and a bit of math to figure out where someone clicked within a grid. But I didn't realize that 11-year-olds haven't worked with coordinate spaces, and thus the math quickly overwhelmed Tim.
The next roadblock was Java. Yes, Java is a simple language to learnif you already know another C-based language. I did, when I learned Java. But poor Tim's school system had already failed him by neglecting to ensure that he had completed computer language basics before moving on to the fifth grade. Java also has many words that are too "adult" and abstract for an 11-year-old to grasp, such as interface, void and static. Tim was getting frustrated.
Trying a New Target
We backed off. The number-guessing game ("I'm thinking of a number between one and 100") became our new target. I taught Tim to code the best way I know howby writing the tests first (a.k.a. test-first design, or TfD; see sidebar), and we tackled the number-guessing game. That's another story, one that ends with mixed success: We finished coding the game, but by the end, Tim was ready to return to playing StarCraft and leave the programming to the slightly older kids like Dad.
However, I was still interested in the game of dots, since I hadn't built any GUI code using test-first design. How would my code evolve were I to build it from scratch with TfD?
Where to Start?
I thought about the game of dots and made a mental sketch of a preliminary design. I came up with a Swing user interface class that contains the view (output) and controller (managing the mouse input). The model class contains the game's basic concepts: a grid of dots that can be connected by lines, a list of players and turn tracking. The big question was how to handle all the stuff that would normally be considered GUI codespecifically, the hit-testing: Where do users click, and what should happen when they click there?
I suspected that hit-testing would be the most complex part of the application, so I wanted to ensure that I had tests for it. It also didn't belong in the model, since that should contain only basic game concepts. I needed an intermediary class somewhere: The Swing code would get a mouse event and delegate it to this intermediary class. The intermediary would interpret the mouse coordinates and determine if something had to happen. It would possibly do this by interacting with the model. If anything did happen, an event would be broadcast back to the Swing class.
The Testing Follies [click for larger image] When you're developing GUIs (including Web pages) test-first, the need to constantly crank up the GUI itself diminishes. My real jollies come from seeing the tests run. My JUnit green bar gave me confidence that the underlying model was doing its job properly.
|
I came up with a set of initial names for the classes: Board for the underlying model, BoardPanel for the JPanel that would represent the visual interface, and (for lack of a better name) BoardController for the intermediary class. Ultimately, the name for this class became BoardPresenter (more on this later). I also later renamed the Board class to Game.
With the initial design sketch in place, I wanted to minimize the Swing code as much as possible; first, because I'm not greatly enamored of SwingI find it to be fairly painful at times. Second, I wanted to get the code so small and stable that it would never have to change, and it couldn't possibly break.
Drawing Dots
Drawing a grid of dots was the first UI problem I tackled. I built enough of a Game object to support a grid size (for example, a 3x3 grid is size 3).
I wanted the UI to send a single message to the presenter to tell it to initialize the board. The presenter would contain the physical characteristics of the game (space between dots, offset from the left-hand side and so on). It would use this information to iterate through the grid size of the game and calculate dot locations. For each dot to be drawn, the presenter would send a message to an interested listener to draw a dot at a specific coordinate.
From this proposed solution, I incrementally built a test to specify it. The code below shows a portion of the test written for drawing dots. (I've abbreviated some of my descriptive variable names to accommodate the narrow column sizes.)
final Set gotDots = new HashSet(); BoardPresenterDrawListener listener = new BoardPresenterDrawAdapter() { public void drawDot(int x, int y, int size) { gotDots.add(new Dot(x, y, size)); }}; BoardPresenter presenter = new BoardPresenter(new Game(3), listener); Set needDots = new HashSet(); int size = presenter.getDotSize(); int halfSize = size / 2; int numDots = gameSize + 1; for (int i = 0, x = _leftX; i < numDots; i++, x += _side) for (int j = 0, y = _topY; j < numDots; j++, y += _side) needDots.add(new Dot(x - halfSize, y - halfSize, size)); presenter.initializeBoard(); assertSetEquals(needDots, gotDots);
Stepping through this code, line by line:
- Create a set, gotDots, to store the parameters for drawDot message sends.
- Create a mock implementation of a BoardPresenterDrawListener. The job of this anonymous inner class definition is to capture any drawDot messages sent and store the parameter information in gotDots.
- Create a BoardPresenter, passing in a Game (with an arbitrary size of 3) and the mock listener.
- Create a set, needDots, to store the dots that are expected.
- Create a temporary variable to store the size of a dot.
- Create a temporary to store the size of half a dot.
- Create a temporary that specifies the number of dots to draw in each direction.
- Loop through the number of dots in both directions, incrementing
x
andy
by the size of a side. For eachx
andy
coordinate, create a new dot object. Since each (x, y
) coordinate specifies the center point of a dot's expected location, determine the starting point for drawing the dot by subtracting the size of half a dot from bothx
andy
. Store the dot in needDots. These are the dots we expect! - Tell the presenter to initialize the board. At this point, I coded just enough to get things to compile. I ran the test, expecting that it would fail. It did. I then coded
initializeBoard()
to send the dots (see code below). - Ensure that we got the dots we needed.
Once I got the test working, I modified it to also test against a different board size. I was ready to write Swing code!
There wasn't much to it. I first ensured that the BoardPanel acted as a
BoardPresenterDrawListener
:public class BoardPanel extends JPanel implements BoardPresenterDrawListener
Somewhere in the initialization code of
BoardPanel
, I constructed aBoardPresenter
, passing in this as the listener:_presenter = new BoardPresenter(game, this);
I then added the code to initialize the board:
_presenter.initializeBoard(); repaint();
I chose to manage the image presentation using double-buffering. Thus, all draws are made to an offscreen graphics context. My final job in getting the dots drawn was to implement the
drawDot
method, per theBoardPresenterDrawListener
interface specification:public void drawDot(int x, int y, int size) { _offscreenGraphics.fillOval(x, y, size, size); }
No logic! The only thing that
drawDot
has to do is delegate the message to the offscreen graphics context. Once I've seen this work, I'm confident that this can't break.
public void initializeBoard() { int dotSize = getDotSize(); int halfDot = dotSize / 2; for (int i = 0; i < _game.getSize() + 1; i++) for (int j = 0; j < _game.getSize() + 1; j++) { int dotX = (getLeftOffset() + i * getSideLength()) - halfDot; int dotY = (getTopOffset() + j * getSideLength()) - halfDot; listener.drawDot(dotX, dotY, dotSize); } }
Drawing Lines
The toughest part, figuring out how the objects were going to talk to each other, was behind me now. I figured that the line drawing would work much the same: The presenter would process a click, and, if a line were drawn, send a message with start and end points to the listener.
The second most difficult part was figuring out what to do with a click. I tried to sit down and write testClick
, but realized that it was too big a method to tackle. The x
and y
coordinates of a click could represent one of many things: The click was exactly on a line, the click was out-of-bounds, the click was close to a line and so on. I started with the simplest case: The click was out-of-bounds, and nothing should happen.
public void testClickOutOfBounds() { int x = _leftX - 10; int y = _topY; assertTrue(_presenter.isOutOfBounds(getEvent(x, y))); y += (_side * _presenter.getBoard().getSize()) + _tolerance; assertTrue(_presenter.isOutOfBounds(getEvent(x, y))); }
The idea of these click tests is straightforward: First specify x
and y
coordinates for a click to emulate. Then directly construct a MouseEvent
with these coordinates (using my utility method getEvent)
. testClickOutOfBounds
sends the MouseEvent
along with the message isOutOfBounds
, which returns a boolean.
Once I built the first assertion in this test, I coded the simplest thing possible in BoardPresenter
to get it to work: Have isOutOfBounds
always return true. I then added another set of x
and y
coordinates and a second assertion to the test. I enhanced the logic to get this test to pass.
After testClickOutOfBounds
, I went on to more tests:
testClickInBounds testClickOnDotCornerIsAmbiguous testClickOffCornerIsNotAmbiguous testClickOffBothLinesIsTooFarOff testClickOnOneLineIsNotTooFarOff testXClickIsOnLine testXClickIsOffLine testYClickIsOnLine testYClickIsOffLine
I then wrote testValidClick
to ensure that the method isValid
returned true only if the click was not out of bounds, not ambiguous and not too far off a line. Now I was able to go back and flesh out testClick
.
Ultimately, testClick
worked like the dots test: Create a mock listener, emulate a mouse click and expect that the mock listener receives all the drawLine
messages expected. For a click on what should be a line, the presenter code would have to send a drawLine
message to interested parties.
You'll note that there's a distinction between x
and y
clicks. It turned out that the easiest way to build incrementallyand not wait too long between successful test runswas to break the logic down so that I tested the x
coordinate separate from the y
.
final List lines = new ArrayList(); BoardPresenterDrawListener listener = new BoardPresenterDrawAdapter() { public void drawLine(int x0, int y0, int x1, int y1) { lines.add(new Line(x0, y0, x1, y1)); }}; BoardPresenter _presenter = new BoardPresenter(new Game(3), listener); _presenter.processClick(getEvent(getArbitraryPointOnUlNorth())); assertEquals(1, lines.size()); assertEquals(getUlNorth(), lines.get(0)); _presenter.processClick(getEvent(getArbitraryPointOnUlWest())); assertEquals(2, lines.size()); assertEquals(getUlWest(), lines.get(1));
The abbreviation Ul in getArbitraryPointOnUlNorth()
stands for upper leftthe left-most and top-most cell in any dots grid. The method itself returns an arbitrary point on the north line of the upper-left cell. I created a handful of utility methods to represent common points in testing. The data structure I ultimately used to store game information was based on the concept of a two-dimensional array of cells, each having a north, east, south and west line.
The code in the GUI was twice as complex: I had to ensure that a repaint occurred each time a line was drawn.
public void drawLine(int xFrom, int yFrom, int xTo, int yTo) { _offscreenGraphics.drawLine(xFrom, yFrom, xTo, yTo); repaint(); }
Drawing Initials
Finally, I built code to ensure that initials were being sent properly when a box was closed. I used the same concept as drawLine
and drawDot
, except that now I would have to emulate a series of mouse clicks before a drawInitials
message was sent.
The code now required interaction with the Game class. As a line was clicked, I had to track that somehow in the Game object. I diverted my attention to GameTest
, building a testBoxClose
method, such that the presenter was a listener on the Game, and was notified when the box closed; the listener on the presenter was subsequently notified to draw the appropriate initials.
The GUI code ended up being two lines of code, instead of one.
public void drawInitials(Point center, String initials) { Point startOn = _presenter.getStartForString( center, initials, _offscreenGraphics); _offscreenGraphics.drawString(initials, startOn.x, startOn.y); }
Why two lines? Because it was easiest to send the centerpoint of the initials along with the drawInitials
message, not the lower-left corner at which the Graphics method drawString
expectseasiest not because the production code would otherwise be more difficult, but because the tests themselves would have been considerably more complex.
The problem? In order to determine the starting point of a string, it must be rendered in a graphics context. In order to render it, the string actually has to be drawn on-screenat least that's what I discovered, based on my knowledge of Swing and experimentation. This means that the test must construct a frame and panel, create an image, set the font, get the font metrics and so on. Not only are these manipulations a bear to manage in tests, they result in the test actually flashing windows on-screen.
I resigned myself to constructing the frame in order to test getStartForString
(used in drawInitials
above). Beyond that, I didn't want to clutter my test executions with lots of flashing windows.
I'm not sure I like this trade-off, but it's what I ended up with. I'll fix it the next time I have to touch that portion of the code.
Evolving Into Patterns
Recall that, for lack of a better name, I initially named the intermediary class BoardController instead of BoardPresenter. I knew it technically wasn't a controller, since that functionality was handled by the Swing code in BoardPanel.
I did a search on Google, and discovered that I had developed a pattern known as Model-View-Presenter (MVP), a variant of Model-View-Controller (MVC). MVP was mined at IBM and is used heavily in the architecture of Dolphin Smalltalk (www.object-arts.com/DolphinSmalltalk.htm)
A Deceptively Simple GUI |
In MVP, the View actually serves as both the view and controller (presenting output and managing input). The Model is the Model, as in MVC. The extra middle layer is considered a bridge between the View and the Model. It's specific to the application and is often considered throwaway if the View needs to change (for example) from Swing to HTML. In this situation, the Model would remain unchanged.
Once I realized that I had unwittingly implemented MVP, I had the appropriate name for my intermediary class: BoardPresenter.
Connecting the Dots
There were at least four cool things about this exercise. First, I had built as much as I wanted to, test-first, and ended up with 32 JUnit tests running. I didn't have tests against the actual GUI code, but I was satisfied that what I had built couldn't possibly break.
This satisfaction comes about because of the second cool thing: the extremely simple BoardPanel, which represents the bulk of the visual interface. The source file for BoardPanel contains 56 lines, and that's nicely formatted with blank lines. The bulk of the display work is in three methods, each only one or two lines long. The only real complexity is in managing the double-buffering.
The evolution of the code into the recognized MVP pattern was cool thing #3. The last cool thing: When you're developing GUIs (including Web pages) test-first, the need to constantly crank up the GUI itself diminishes. My real jollies come from seeing the tests run. My JUnit green bar gave me confidence that the underlying model was doing its job properly. I've seen developers wait until the end of the day before kicking off the actual GUI, along with a "Ho-hum, of course it works."
No ho-hums here: This exercise was a blast for me. Here's what the game of dots looks like (see "A Deceptively Simple GUI," above).
Click here to download a zip file of the complete code. Please note that you will need JUnit (www.junit.org).