Channels ▼
RSS

Connecting the Dots

, June 01, 2002


June 2002: Connecting the Dots June 2002

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 made—first trying the game of dots and finally settling on a number-guessing game—were 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 dice—Tim 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 learn—if 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 how—by 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 code—specifically, 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 Swing—I 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:

  1. Create a set, gotDots, to store the parameters for drawDot message sends.
  2. 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.
  3. Create a BoardPresenter, passing in a Game (with an arbitrary size of 3) and the mock listener.
  4. Create a set, needDots, to store the dots that are expected.
  5. Create a temporary variable to store the size of a dot.
  6. Create a temporary to store the size of half a dot.
  7. Create a temporary that specifies the number of dots to draw in each direction.
  8. Loop through the number of dots in both directions, incrementing x and y by the size of a side. For each x and y 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 both x and y. Store the dot in needDots. These are the dots we expect!
  9. 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).
  10. 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);
        }
    }
    

  11. 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 a BoardPresenter, 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 the BoardPresenterDrawListener 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.

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 incrementally—and not wait too long between successful test runs—was 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 left—the 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 expects—easiest 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-screen—at 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).



A Capsule Guide to Test-First Design
How to minimize your dependencies and produce super-strong code.

How does test-first programming work, anyway? It's quite simple:

  • Write a test that specifies a tiny bit of functionality.
  • Ensure that the test fails (you haven't built the functionality yet!).
  • Write only the code necessary to make the test pass.
  • Refactor the code to ensure that it has the simplest design possible for the functionality built to date.

Follow these three simple rules:

  • Test everything that can possibly break.
  • Tests come first.
  • All tests must run before you can check in code.

That's it. This process offers some significant benefits:

  • Code is written so that modules are testable in isolation. This is in contrast to the bulk of code out there. Code written without tests in mind is often highly coupled, which is one of the biggest hints that you have a poor object-oriented design. If you write tests first, you'll come up with ways of minimizing dependencies in your system in order to write those tests.
  • The tests act as system-level documentation of the capabilities of your class. They are the first client of your classes; they show how the developer intended the class to be used.
  • The system has automated tests, by definition. Running full regression tests manually will ultimately take outrageous amounts of time as your system grows.
  • Development is paced. We bite off tiny bits of functionality to specify in our tests, then we write a small amount of code to fulfill the specification of the test. Rinse, repeat. By tiny, I mean seconds to a few minutes. The code base progresses at a relatively constant rate in terms of the functionality supported.

For more information on TfD, read Kent Beck's Extreme Programming Explained (Addison-Wesley, 1999) and Ronald E. Jeffries, Ann Anderson and Chet Hendrickson's Extreme Programming Installed (Addison-Wesley, 2000).

—J. Langr

 


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video