Are Uppman
Home>bowling>article

Don't forget the specification

by Are Uppman

 

Abstract: Many people recommend writing the tests for a code before writing the code. "Extreme Programming" takes this to a limit. In this article I emphasise that it's just as important to specify the code before developing the tests. The developer gets large benefits in return.

Introduction

Robert C. Martin and Robert S. Koss present in [1] a very interesting example of Extreme Programming (XP) [2]. The article is made very lively by the discussion caught on the fly by the two authors.

A first reaction on that article, written by Leo Scott and Wayne Conrad, may be found at [3]. Elsewhere a discussion is going on in a forum [4] on the merits of XP. Refer especially to [5].

Since I look at examples of programming and testing on Internet I am struck by how little specification, if any, is required before writing the test. This fact is all the more astonishing as I would like to ask: makes it any sense to write a test without having any specification ?

Of course XP states that it is not the panacea. And I can imagine that one might in some cases undertake coding, lets say as a play, with only a vague specification present in thought and not yet worked out. But to test without at least some considering on the specification and the expression of this one, seems to me miss something essential:

no discussion concerning the qualities of the test may be done without the knowledge of this specification

the specification allows us to do a cleaner analysis and design.

This latter point is very important. The experience teaches us that when we develop a set of classes without first specifying these classes, then if we try to specify afterwards, this brings on a new understanding of the role of each class and method, witch in turn most often imply a profound reformulation of the total architecture. Conclusion: development without specification gives most often a poor architecture.

And that's just why we teach our students to code this way:

formulate the need, delimit, specify, specify tests, analyse and design, code, test.

So I should like to propose my contribution to this debate by presenting a more "academic" walk along this way, which is rather different from the one of Martin and Koss.

The need

Let's imagine a demand like this:

One wishes to make a program that shows the scores of a bowling game. More precisely, one wishes to have some means to register on the fly the number of pins knocked down by each throw, and in return gather the information used in the publishing of the score.

I must confess that I have never played bowling, and that when I read the cited articles, I didn't know the rules nor did I know how to count the score. Maybe you can be able to guess these elements from the reading of the two articles, but as the authors code without specifying, you hardly could do better than a guess.

So I took a trip on the Web to learn about bowling, and here's my way of presenting the rules and the counting. You should note that next paragraph is more than a reminder: it will be incorporated into the class BowlingGame (see further on) as part of the specification, because it defines many words that the rest of the specification uses.

The rules of bowling

I suppose we all know that bowling is played on a lane with at the end 10 pins that the player should knock down with a ball that he throws and makes roll on the lane.

The placing of the ten pins makes up what we call a frame. The player has two throws to try to knock down the 10 pins of a frame. If he knocks down all the ten pins by the first throw we say he makes a strike, and the second throw has no reason to be. If he has knocked down all the ten pins after the second throw, we say he makes a spare.

In this article we will say that a frame is

A game is, for each player, made up of 10 frames. The total score is calculated as the sum of the individual score of each frame. This individual score is calculated as follows:

This counting may demand to play one or two extra throws to complete the score of the 10th frame (one extra throw in case of spare and two in case of strike). Extra throws means extra frames (two if the 10th frame is strike and the first extra throw is a strike to).

Delimit the domain

So let's suppose our task is to build the structure – a sort of container class – on which a Graphical User Interface (GUI) could be based to present the evolving game and score of a player.

Traditionally such a presentation shows the following elements:

The presentation is divided into 10 rectangles, one for each frame. Each rectangle is divided into three sub-rectangles: two upper small ones and one lower large. The first small rectangle is void in case of strike and shows the pins of the first throw otherwise. The second shows a diagonal cross in case of strike and a diagonal bar in case of spare after the second throw, and nothing else. The third, the bigger one, shows the total cumulated score including this frame once the frame is ready, and nothing otherwise.

In fact, to the right of the 10th rectangle there may be one or two small extra sub-rectangles. They show the pins of the supplementary throw(s) in case of strike or spare (there is no great rectangle, however, as there is no new cumulated score for the corresponding extra frames).

Let's recall that our goal is not to realise this GUI, but only a class offering all the functionality that the GUI could wish in order to register throws and extract the information it needs to draw the rectangles. To do this, the GUI typically would address the frames by index (1 to 10).

The specification

To satisfy the demands, our container class should offer

We could also offer a method individualScore for a given ready frame. However, individualScore and cumulatedScore are obviously redundant.

Those are maybe the most evident methods, but they are not sufficient to draw the rectangles of the presentation.

In fact, the great rectangle should stay empty if it's frame is not ready, and to draw the small rectangles, we must know if the state of the frame is a strike, a spare, or neither strike nor spare. In short, we must be able to tell the state of the frame. So our container class should offer

We distinguish the following states for a frame:

     first throw not yet done
     strike, awaiting second throw
     strike, awaiting third throw
     ready after strike
     first throw done (not strike), before second
     spare, awaiting third throw
     ready after spare
     ready after neither strike nor spare

To draw the content of the two small rectangles of the presentation for a given frame, our class should also offer a method returning the pins of the throws contributing to the individual score of a frame:

We note that several methods have some limitations that must be taken into account

it makes no sense to call add when the game is completed

it is impossible to knock down more than 10 pins for a given frame (nor less than zero!)

there is no total score for a frame that is not yet ready

there is no n-th contributing throw for a frame that is not yet in the corresponding state

there is no frame outside the interval [1, 10] (we don’t allow to address directly any extra frame for extra throws).

We know that all limitations on the use of methods usually imply the specification of appropriate exceptions.

From these conclusions we can formulate the following specification

     public class BowlingGame {
       BowlingGame lets you follow up the game of a bowling player, that is
       register throws and establish the frames and the scores of the game.
     
       public BowlingGame() 
         Creates a BowlingGame ready to begin the counting. 
         The state of all ten frames (frameState) is 0.
     
       public void add(int pins)
         Registers the pins knocked down by a throw. 
         parameter pins : the number of pins to register.
         throws IndexOutOfBoundsException if the game is complete
         throws IlleagalArgumentException if on the first throw for a frame the  
               number N = pins does not belong to [0, 10] or if on the second 
               throw "pins" does not belong to [0, 10 - N] 
       
       public int cumulatedScore(int index) 
         Gives the cumulated score up to the given frame if the frame is ready.
         returns the score
         throws IndexOutOfBoundsException if index doesn't belong to [1, a] where 
               a is the number of frames that are ready
       
       public int frameState(int index) 
         Gives the state of a frame. 
         The state is 
           0 if first throw is not yet done
           1 if strike and awaiting second throw
           2 if strike and awaiting third throw
           3 if ready after strike
           4 if first throw done (not strike) and before second
           5 if spare and awaiting third throw 
           6 if ready after spare
           7 if ready after neither strike nor spare
         parameter index : the index of the frame.
         returns the state of the frame
         throws IndexOutOfBoundsException if the index of the frame does not 
                belong to [1, 10]
       
       public int throwForFrame(int index, int n) 
         Gives the number of pins of the n-th throw contributing to the individual
         score of a frame.
         parameter index : the index of the frame
         parameter n : the order of the contributing throw for this frame (1st,
         2d or even 3d) 
         returns the number of pins of the n-th contributing throw of the frame
         throws IndexOutOfBoundsException if the frame index does not have
                a n-th contributing throw
     }
     

Black box testing

We now have our specification. It's time to specify the tests we want to run on our code.

Is it possible to do an exhaustive test ? Probably not, because there are more than 6610 sequences of possible throws, and I see no simple oracle to use (the oracle tells, as we know, the truth. Here it should tell if any result of an execution conforms to the specification, yes or no).

Having no simple oracle it isn't possible to make a random test either.

We are thus restricted to apply simple and classical testing principles like the partition principle or the limit principle to guide us when selecting data for the black box tests. Let's take the method specifications one after the other.

No variation on the data entries is possible, our test will be

     Scenario               Awaited result	
     		
     create a BowlingGame   for all frames i, frameState(i) == 0	
     

Note: when we realise a scenario like g = new BowlingGame(); g.add(10); g.add(7); g.add(3); we shall write "10 7 3" this scenario.

This is the most complex method of the class. We must remember that add really takes two parameters: the pins of the throw and the BowlingGame object on which add is invoked. The latter has a complex state that add will modify by modifying the states of the one to three frames concerned. How can we partition the entries to add in a useful way ? The following criteria might be used:

the index of the current frame

the state of the current frame

the number of pins (add's normal parameter)

the number of the other frames concerned by the add

the states of the other frames concerned by the add

the special case of the 10th frame

the exceptions.

The combination of these criteria would result in a test with hundreds of entries. To simplify, I propose to replace them with

the different state transitions that a given frame may undergo

the special case of the 10th frame

the exceptions.

The first two are covered by the test

     Scenario                                     Awaited result	
                                     frameState(1) frameState(2) frameState(3)
     d0  = ""                              0             0             0
     d1  = "10"                            1             0             0
     d2  = "10 3"                          2             4             0   
     d3  = "10 3 7"                        3             5             0
     d4  = "10 3 7 8"                      3             6             4
     d5  = "10 3 7 8 1"                    3             6             7

     d6  = "10 3 7 8 1 10 10 10 10 10 10" (9 frames)    similar for 8, 9, 10
     d7  = "10 3 7 8 1 10 10 10 10 10 10  10"            ...
     d8  = "10 3 7 8 1 10 10 10 10 10 10  10 9"          ...
     d9  = "10 3 7 8 1 10 10 10 10 10 10  10 9 1"        ...
     d10 = "10 3 7 8 1 10 10 10 10 10 10   8"            ...
     d11 = "10 3 7 8 1 10 10 10 10 10 10   8  2"         ...
     d12 = "10 3 7 8 1 10 10 10 10 10 10   8  2 10"      ...
     d13 = "10 3 7 8 1 10 10 10 10 10 10   8  1"         ...
     

The exceptions are covered by the following scenarios where we have also verified that the state has not been changed when an exception was thrown (this normally is an implicit requirement).

     Scenario                                     Awaited result	
     	
     d14 = "10 -1"                                IllegalArgumentException
                                                  frameState 1 0 0
     d15 = "10 11"                                IllegalArgumentException
                                                  frameState 1 0 0
     d16 = "10 8 3"                               IllegalArgumentException
                                                  frameState 2 4 0
     d16 = "10 8 2 11"                            IllegalArgumentException
                                                  frameState 3 5 0
     d17 = "10 10 10 10 10 10 10 10 10   8  1 10" IndexOutOfBoundsException
     

frameState

We just employed frameState in the test for add. In fact, that test is also a test for frameState. No other test seems necessary.

cumulatedScore

The following criteria might be used:

frame is a strike, a spare, neither strike nor spare

the special case of the 10th frame

the exception

Because the score is cumulated, the first criteria may be included in the second. We thus use

     Scenario         Awaited result	
     
     d9               cumulatedScore(1) == 20
                      cumulatedScore(2) == 38 
                      cumulatedScore(3) == 47
                      cumulatedScore(8) ==197
                      cumulatedScore(9) ==226 
                      cumulatedScore(10)==246
     d12              cumulatedScore(8) ==195
                      cumulatedScore(9) ==215 
                      cumulatedScore(10)==235
     d13              cumulatedScore(8) ==195
                      cumulatedScore(9) ==214 
                      cumulatedScore(10)==223

and for the exceptions

     Scenario         Awaited result	
     
     d0               cumulatedScore(1) throws IndexOutOfBoundsException
     d8               cumulatedScore(10) throws IndexOutOfBoundsException

throwForFrame

The criteria could be

for a given frame, strike (n=1, 2 or 3), spare (n=1, 2 or 3), neither strike nor spare (n=1, 2)

for a given frame, the value of n (1, 2, 3)

the special case of the 10th frame

These are covered by

     Scenario         Awaited result	
     
     d5               throwForFrame(1, 1) == 10
                      throwForFrame(1, 2) ==  3
                      throwForFrame(1, 3) ==  7
                      throwForFrame(2, 1) ==  3
                      throwForFrame(2, 2) ==  7
                      throwForFrame(2, 3) ==  8
                      throwForFrame(3, 1) ==  8
                      throwForFrame(3, 2) ==  1
     d9               throwForFrame(10, 1) == 10
                      throwForFrame(10, 2) ==  9
                      throwForFrame(10, 3) ==  1
     d12              throwForFrame(10, 1) ==  8
                      throwForFrame(10, 2) ==  2
                      throwForFrame(10, 3) == 10
     d13              throwForFrame(10, 1) ==  8
                      throwForFrame(10, 2) ==  1
     

and the exceptions

     Scenario         Awaited result	
     
     d0               throwForFrame(1, 1) throws IndexOutOfBoundsException 
     d1               throwForFrame(1, 2) throws IndexOutOfBoundsException 
     d2               throwForFrame(1, 3) throws IndexOutOfBoundsException 
     d3               throwForFrame(1, 4) throws IndexOutOfBoundsException 
     d12              throwForFrame(2, 4) throws IndexOutOfBoundsException 
                      throwForFrame(3, 3) throws IndexOutOfBoundsException 
                      throwForFrame(10, 3) throws IndexOutOfBoundsException 

The test program

The above specification of tests is directly translated into the following monolithic Java program (it is easy to convert this class to conform to the JUnit [6] testing framework).

     import java.util.StringTokenizer;
     
     public class TestBowlingGameGlobal { 
       public static void main(String[] args) {
         TestBowlingGameGlobal t = new TestBowlingGameGlobal();
         t.testBowlingGame();
         t.testAddAndFrameState();
         t.testAdd_Exceptions();
         t.testCumulatedScore();
         t.testCumulatedScore_Exceptions();
         t.testThrowForFrame();
         t.testThrowForFrame_Exceptions();
       }
       void assertTrue(boolean b) {
         if (! b) throw new Error("Erreur d'assertTrue");
       }
       void fail(String message) {
         throw new Error(message);
       }
       void set(String s) {
         g = new BowlingGame();
         StringTokenizer st = new StringTokenizer(s);
         while (st.hasMoreTokens()) {
           int v = Integer.parseInt(st.nextToken());
           g.add(v);
         }
       }
       BowlingGame g; 
       public void testBowlingGame() {
         g = new BowlingGame(); 
         for (int i = 1; i <= 10; i++)
           assertTrue(g.frameState(i) == 0);
       } 
       public void testAddAndFrameState() { 
         set("10");
         assertTrue(g.frameState(1) == 1);
         assertTrue(g.frameState(2) == 0);
         assertTrue(g.frameState(3) == 0);
         set("10 3");
         assertTrue(g.frameState(1) == 2);
         assertTrue(g.frameState(2) == 4);
         assertTrue(g.frameState(3) == 0);
         set("10 3 7");
         assertTrue(g.frameState(1) == 3);
         assertTrue(g.frameState(2) == 5);
         assertTrue(g.frameState(3) == 0);
         set("10 3 7 8");
         assertTrue(g.frameState(1) == 3);
         assertTrue(g.frameState(2) == 6);
         assertTrue(g.frameState(3) == 4);
         set("10 3 7 8 1");
         assertTrue(g.frameState(1) == 3);
         assertTrue(g.frameState(2) == 6);
         assertTrue(g.frameState(3) == 7);
         set("10 10 10 10 10 10 10 10 10");
         assertTrue(g.frameState(8) == 2);
         assertTrue(g.frameState(9) == 1);
         assertTrue(g.frameState(10) == 0);
         set("10 10 10 10 10 10 10 10 10  10");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 2);
         assertTrue(g.frameState(10) == 1);
         set("10 10 10 10 10 10 10 10 10  10 9");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 3);
         assertTrue(g.frameState(10) == 2);
         set("10 10 10 10 10 10 10 10 10  10 9 1");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 3);
         assertTrue(g.frameState(10) == 3);
         set("10 10 10 10 10 10 10 10 10  8");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 2);
         assertTrue(g.frameState(10) == 4);
         set("10 10 10 10 10 10 10 10 10  8 2");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 3);
         assertTrue(g.frameState(10) == 5);
         set("10 10 10 10 10 10 10 10 10  8 2 10");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 3);
         assertTrue(g.frameState(10) == 6);
         set("10 10 10 10 10 10 10 10 10  8 1");
         assertTrue(g.frameState(8) == 3);
         assertTrue(g.frameState(9) == 3);
         assertTrue(g.frameState(10) == 7);
       }
       public void testAdd_Exceptions() { 
         try {
           set("10 -1");
           fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException iae) {
         }
         assertTrue(g.frameState(1) == 1);
         assertTrue(g.frameState(2) == 0);
         assertTrue(g.frameState(3) == 0);
         try {
           set("10 11");
           fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException iae) {
         }
         assertTrue(g.frameState(1) == 1);
         assertTrue(g.frameState(2) == 0);
         assertTrue(g.frameState(3) == 0);
         try {
           set("10 8 3");
           fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException iae) {
         }
         assertTrue(g.frameState(1) == 2);
         assertTrue(g.frameState(2) == 4);
         assertTrue(g.frameState(3) == 0);
         try {
           set("10 8 2 11");
           fail("Should throw IllegalArgumentException");
         } catch (IllegalArgumentException iae) {
         }
         assertTrue(g.frameState(1) == 3);
         assertTrue(g.frameState(2) == 5);
         assertTrue(g.frameState(3) == 0);
         try {
           set("10 10 10 10 10 10 10 10 10   8  1 10");
           fail("Should throw IndexOutOfBoundsException");
         } catch (IndexOutOfBoundsException iobe) {
         }
       }
       public void testCumulatedScore() {
         set("10 3 7 8 1 10 10 10 10 10 10  10 9 1");
         assertTrue(g.cumulatedScore(1) == 20);
         assertTrue(g.cumulatedScore(2) == 38);
         assertTrue(g.cumulatedScore(3) == 47);
         assertTrue(g.cumulatedScore(8) == 197);
         assertTrue(g.cumulatedScore(9) == 226);
         assertTrue(g.cumulatedScore(10) == 246);
         set("10 3 7 8 1 10 10 10 10 10 10  8 2 10");
         assertTrue(g.cumulatedScore(8) == 195);
         assertTrue(g.cumulatedScore(9) == 215);
         assertTrue(g.cumulatedScore(10) == 235);
         set("10 3 7 8 1 10 10 10 10 10 10  8 1");
         assertTrue(g.cumulatedScore(8) == 195);
         assertTrue(g.cumulatedScore(9) == 214);
         assertTrue(g.cumulatedScore(10) == 223);
       }
       public void testCumulatedScore_Exceptions() {
         set(""); 
         try {
           g.cumulatedScore(1);
           fail("Should throw IndexOutOfBoundsException");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 10 10 10 10 10 10 10 10  10 9");
         try {
           g.cumulatedScore(10);
           fail("Should throw IndexOutOfBoundsException");
         } catch (IndexOutOfBoundsException iobe) {
         }
       }
       public void testThrowForFrame() {
         set("10 3 7 8 1");
         assertTrue(g.throwForFrame(1, 1) == 10);
         assertTrue(g.throwForFrame(1, 2) ==  3
         assertTrue(g.throwForFrame(1, 3) ==  7
         assertTrue(g.throwForFrame(2, 1) ==  3
         assertTrue(g.throwForFrame(2, 2) ==  7
         assertTrue(g.throwForFrame(2, 3) ==  8
         assertTrue(g.throwForFrame(3, 1) ==  8
         assertTrue(g.throwForFrame(3, 2) ==  1
         set("10 3 7 8 1 10 10 10 10 10 10  10 9 1");
         assertTrue(g.throwForFrame(10, 1) == 10
         assertTrue(g.throwForFrame(10, 2) ==  9
         assertTrue(g.throwForFrame(10, 3) ==  1
         set("10 3 7 8 1 10 10 10 10 10 10   8  2 10");
         assertTrue(g.throwForFrame(10, 1) ==  8
         assertTrue(g.throwForFrame(10, 2) ==  2
         assertTrue(g.throwForFrame(10, 3) == 10
         set("10 3 7 8 1 10 10 10 10 10 10   8  1");
         assertTrue(g.throwForFrame(10, 1) ==  8
         assertTrue(g.throwForFrame(10, 2) ==  1
       }
       public void testThrowForFrame_Exceptions() {
         set("");
         try {
           g.throwForFrame(1, 1); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10");
         try {
           g.throwForFrame(1, 2); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3");
         try {
           g.throwForFrame(1, 3); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3 7");
         try {
           g.throwForFrame(1, 4); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3 7 8 1 10 10 10 10 10 10   8  2 10");
         try {
           g.throwForFrame(1, 4); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3 7 8 1 10 10 10 10 10 10   8  2 10");
         try {
           g.throwForFrame(2, 4); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3 7 8 1 10 10 10 10 10 10   8  2 10");
         try {
           g.throwForFrame(3, 3); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
         set("10 3 7 8 1 10 10 10 10 10 10   8  2 10");
         try {
           g.throwForFrame(10, 4); 
           fail("Should throw IndexOutOfBoundsException ");
         } catch (IndexOutOfBoundsException iobe) {
         }
       }
     } 
     

The analysis and design

It seems rather natural to build the BowlingGame class on the ten frames of a game plus maybe two extra frames, and on the notion of a current frame: the frame whose pins are now on the lane and which might be hit by the next throw. These frames will refer to a common sequence of throws (or number of pins), maximum 21 throws are possible, and the notion of a current throw. That's why the beginning of our code of BowlingGame will look like this:

     public class BowlingGame {
       final int maxFrames = 10+2; // 10 ordinary, 2 extra
       final int maxThrows = 21;
       Frame[] frames = new Frame[1 + maxFrames]; // 0 not used 
       int currentFrame = 1; // current index to frames
       int[] pins = new int[1 + maxThrows]; // 0 not used
       int currentThrow = 1; // current index to pins
        
       // ...
     
     }
     

To go further we must get a clearer understanding of the class Frame that BowlingGame uses. This means that we must now specify Frame. This in turn means that we must discover all the Frame methods that our class BowlingGame could need. To this end, we shall now imagine a plausible scenario for each method of BowlingGame from its specification.

This constructor should construct the ten frames that BowlingGame manage. These frames are all created in the state 0 (no throw done) and with a reference to the common sequence of pins.

=> Frame should offer a constructor Frame(int[] t).

The account of the pins may concern three frames (current and waiting). Add must invoke on these frames a method that we shall call update to make them modify their state consequently. update drives all the modifications a Frame can undergo, and these modifications depend on the current index into the list of throws (or pins), so update must take this index as a parameter.

=> Frame should offer the method void update(int index).

When should add increment currentFrame ? That depends: has current frame finished ?

=> Frame should offer the method boolean finished().

frameState could delegate this demand to a method getState of the frame whose index is given.

=> Frame should offer the method int getState()

This method iterates over the frames from the first towards index and cumulates the individual scores of those which are ready. To get the score of a frame, cumulatedScore invokes getScore on the frame.

=> Frame should offer int getScore()

This demand could be delegated to a method getTrow of the frame whose index is given.

=> Frame should offer the method int getTrow(int n)

 

Specification and design of Frame

The idea should be clear : Frame manages the state of one frame (in fact it's a state machine), and BowlingGame manages the set of the ten frames of a game. Thus BowlingGame should verify that the index for a frame is in the range 1 to 10, but it seems natural that it be the role of Frame to verify that no more than 10 pins could be knocked down in one frame object.

As a result of this design a given frame don't know anything about its neighbouring frames neither does it know the BowlingGame object within which it is contained. But it does know the common sequence of throws and the index of its first throw in this sequence.

We don't bother to limit the number of updates on a frame : when the frame has reached one of the terminating states, it simply ignores the updates. We also allow the invocation of the other methods at any moment, simply getScore returns 0 if no throw has yet been done, and getThrow return –1 if the corresponding throw does not exist. As a consequence the class BowlingGame should throw the appropriate exceptions when necessary.

The class Frame may now be succinctly specified as follows:

       class Frame {
         Manages the state of a bowling frame. Knows the sequence of
         throws to which it refers. Eventually also knows the index 
         its first contributing throw and its individual score
         
         public Frame(int[] sequence)
           Initializes the state to 0
         public void update(int index)
           Updates the state, throws IllegalArgumentException if the total 
           number of pins for this frame is < 0 or > 10
         public int getState()
           Returns the state of the frame
         public int getThrow(int n)
           Returns the number of pins of the n-th contributing throw, 
           returns -1 if there is no such a throw (yet)
         public int getScore()
           Returns the partial or final individual score
         public boolean finished()
           Tells whether all throws aiming the pins of this frame has been 
           done
       }
     
     

We don't need to specify tests for this small class. Indeed, we don't intend to use it outside of BowlingGame, so the testing of BowlingGame will at the same time test Frame.

The code

The coding of BowlingGame and of Frame is now rather simple.

You should notice that the Javadoc comments are just borrowed from the specification of the class. This tells us that if you eventually write such comments (what you really should do!), then the writing of the specification of the class is not time wasted away.

You should also notice that the method update of Frame clearly indicates that Frame is a simple state machine.

     /**
      *   BowlingGame lets you follow up the game of a bowling player, that
      *   means register throws and establish the frames and the scores of the
      *   game.
      *
      *   Let's remind of the following :
      *   
      *   The bowling game is played on a lane with at the end 10 pins that 
      *   the player should knock down with a ball that he throws and make roll
      *   on the lane.
      * 
      *   The placing of the ten pins make up what we call a frame. The player 
      *   has two throws to try to knock down the 10 pins of a frame. If he 
      *   knocks down all the ten pins by the first throw we say he makes a 
      *   strike, and the second throw has no reason to be. If he has knocked 
      *   down all the ten pins after the second throw, we say he makes a spare
      *   
      *   We will say that a frame is
      *   
      *     - current if next throw concerns some of its pins
      *     - finished if all its (one or two) throws are done (and so, except 
      *       for the 10th, a next frame is current)
      *     - waiting if it is finished but not yet countable
      *     - ready if all the throws needed for the calculation of its score 
      *       are done (a strike or a spare need further throws to calculate
      *       the score)
      *
      *   A game is, for each player, made up of 10 frames. The total score is 
      *   calculated as the sum of the (individual) score of each frame. This 
      *   score is calculated as follows :
      *   
      *     - if the player makes a strike the score of the frame is 10 plus 
      *       the number of pins (0 to 20) of the two next throws
      *     - else, if he makes a spare the score of the frame is 10 plus the 
      *       number of pins (0 to 10) of the next throw
      *     - else the score of the frame is the number of pins (0 to 9) 
      *       knocked down by the two throws of the frame.
      *   
      *   This counting may demand to replace the 10 pins one or two more 
      *   times for the 10th frame (1 time in case of spare and two times in 
      *   case of strike). We shall however agree that it's still the 10th
      *   frame that is current.
      */ 

     public class BowlingGame {
       final int maxFrames = 10+2; // 10 ordinary, 2 extra
       final int maxThrows = 21;
       Frame[] frames = new Frame[1 + maxFrames]; // 0 not used 
       int currentFrame = 1; // current index to frames
       int[] pins = new int[1 + maxThrows]; // 0 not used
       int currentThrow = 1; // current index to pins
       
       /**
        *   Creates a BowlingGame ready to begin the counting. 
        *   In particular : the current frame ({@link #currentFrame}) is the 
        *   first one, the state of the frame ({@link #frameState}) is 0.
        */
       public BowlingGame() {
         for (int i = 1; i <= 10; i++) 
           frames[i] = new Frame(pins);
       }
     
       /**
        *   Registers the pins knocked down by a throw. 
        *   @param pins : the number of pins to register.
        *   @throws IndexOutOfBoundsException if the game is complete
        *   @throws IllegalArgumentException if on the first throw for a  
        *           frame the number N = pins does not belong to [0, 10] or if 
        *           on the second throw 'pins' does not belong to [0, 10 - N] 
        */
       public void add(int pins) {
         if ((pins < 0) || (pins > 10))
           throw new IllegalArgumentException("Pins out of bounds "+pins);
         if (isReady(10))
           throw new IndexOutOfBoundsException("Game over!");
           this.pins[currentThrow] = pins;
         for (int i = 0; i <= 2 ; i++) {
           if (currentFrame - i >= 1) {
             frames[currentFrame-i].update(currentThrow);
           }
         }
         currentThrow++;
         if (frames[currentFrame].finished()) {
           currentFrame++;
         }
       }
     
       /**
        *   Gives the cumulated score up to the given frame if the frame 
        *   is ready.
        *   @return the score
        *   @throws IndexOutOfBoundsException if index don't belong to 
        *           [1, a] where a is the number of frames that are ready
        */
       public int cumulatedScore(int index) {
         verifyBounds(index);
         if (!isReady(index))
           throw new IndexOutOfBoundsException("Frame not ready: " + index);
         int sum = 0;
         for ( ; index > 0; index--) 
           sum += frames[index].getScore();
         return sum;
       }
     
       /**
        *   Gives the state of a frame. 
        *   The state is 
        *
        *     0 if first throw is not yet done
        *     1 if strike and awaiting second throw
        *     2 if strike and awaiting third throw
        *     3 if ready after strike
        *     4 if first throw done (not strike) and before second
        *     5 if spare and awaiting third throw 
        *     6 if ready after spare
        *     7 if ready after neither strike nor spare
        *
        *   @param index : the index of the frame.
        *   @return the state of the frame
        *   @throws IndexOutOfBoundsException if the index of the frame does 
        *           not belong to [1, 10]
        */
       public int frameState(int index) {
         verifyBounds(index);
         return frames[index].getState();
       }
       
       /**
        *   Gives the number of pins of the first throw ov a frame.
        *   @param index : the index of the frame
        *   @return the number of pins of the first throw of the frame
        *   @throws IndexOutOfBoundsException if the index does not belong to
        *           [1, a] where a is the number of frames having a first throw
        */
       public int firstThrow(int index) {
         verifyBounds(index);
         int n = frames[index].getFirstThrow();
         if (n < 0) throw new IndexOutOfBoundsException(
           "Index out of bounds: "+index);
         return n;
       }
      
       /**
        *   Gives the number of pins of the n-th contributing throw of a frame.
        *   @param index : the index of the frame
        *   @param n : the rank of the contributing throw
        *   @return the number of pins of the n-th contributing throw of the frame
        *   @throws IndexOutOfBoundsException if the corresponding throw does not
        *           exist or does not concern this frame
        */
       public int throwForFrame(int index, int n) {
         verifyBounds(index);
         int f = frames[index].getThrow(n);
         if (f == -1) 
           throw new IndexOutOfBoundsException("Index out of bounds "+index+" and "+n);
         return f;
       }
      
       /** 
           A frame is ready if and only if all throws needed to complete its 
           score have been done.
       */
       boolean isReady(int index) {
         int s = frameState(index);
         return (s == 3) || (s == 6) || (s == 7);
       }
       
       void verifyBounds(int index) {
         if ((index < 1) || (index > 10))
           throw new IndexOutOfBoundsException("Index out of bounds [1, 10]: "+index);
       }
       
       class Frame {
         int[] pins;
         int state =  0;
         int score =  0;
         int first = -1;
         public Frame(int[] t) {
           pins = t;
         }
         /** 
             This method provokes the transitions of the state machine
          */
         public void update(int pins) {
           switch (state) { 
             case 0  : first = index; 
                       score = pins[index];
                       if (score == 10) state = 1;
                       else state = 4;
                       break;
             case 1  : score += pins[index]; state = 2; 
                       break;
             case 2  : score += pins[index]; state = 3; 
                       break;
             case 4  : if (pins[first] + pins[index] > 10) throw new IllegalArgumentException(
                         "Sum of pins out of bounds: "+pins[first]+" "+pins[index]);
                       score += pins[index];
                       if (score == 10) state = 5;
                       else state = 7;
                       break;
             case 5  : score += pins[index]; state = 6; 
                       break;
             default : ;
           }
         }
         /**
             Returns the number of pins of the n-th contributing throw.
             Returns -1 if the corresponding throw does not concern 
             this frame (yet)
          */
         public int getThrow(int n) {
           if (n <= 0) return -1; // -1 means error
           int p = pins[first - 1 + n];
           switch (state) { 
             case 0: p = -1; break;
             case 1:
             case 4: if (n > 1) p = -1; break;
             case 2:
             case 5:
             case 7: if (n > 2) p = -1; break;
             case 3:
             case 6: if (n > 3) p = -1; break;
           }
           return p;
         }
         /**
             Returns the current state of this frame
             @see BowlingGame.frameState
         */
         public int getState() {
           return state;
         }
         /**
             Returns the current individual score of the frame.
         */
         public int getScore() {
           return score;
         }
         /**
             The frame has finished when the one throw (strike) or two throws 
             (not strike) has been done
         */
         public boolean finished() {
           return (state != 0) && (state != 4);
         }
       }
     }

And next

Of course I was curious to see the GUI part in work, so I ended up with coding an BowlingGameApplet. You find it at [7].

We did nothing to make a Java bean out of BowlingGame. I think it would be more appropriate to reserve this for the GUI part.

But it certainly might be interesting to transform the class into a model class for its GUI part in the sense of Swing. To do this, we should create a BowlingGameEvent and endow the class with methods to manage BowlingGameListeners (or create a brand new BowlingGameModel class based upon or derived from BowlingGame).

Conclusion

The goal of this article was to explore an "academic" way to the BowlingGame class and its testing. Going this way demands that the class being first specified. I hope I have convinced you that this specification brings along numerous advantages

the goal of the code becomes clearer

the goal of the tests becomes clearer, and it's easier to discuss the sufficiency or not of the tests

a true design of the architecture becomes possible

the Javadoc comments are easily deduced.

On the way I have illustrated with this simple example how the analysis of a specification naturally brings up the specification of the other classes on which this one depends.

 

References

[1] The article of Martin and Coss can be found at http://www.objectmentor.com/publications/xpepisode.htm

[2] The Extreme Programming is explained at http://www.objectmentor.com/default.shtml and at http://www.extremeprogramming.org/

[3] You find the article of Scott and Conrad at http://www.leoscott.com/cgi-bin/pywiki?BowlingScores

[4] The discussion forum is "Programming Theory & Practice"

[5] See "Are smart coders the enemy?" at http://www.javaworld.com/javaworld/jw-04-2001/jw-0413-itf-xp_p.html

[6] You might prefer to use the JUnit testing framework at http://www.junit.org/

[7] The GUI part is found at http://areu.free.fr/bowling/BowlingGameApplet.php

 

Author

Are Uppman teaches Program Engineering and Object Oriented Programming at the University of Rouen, France.

He's a member of the

Laboratoire d'Informatique Fondamentale et Appliquée de Rouen

Faculté des Sciences, Université de Rouen

76821 Mont-Saint-Aignan CEDEX, France

e-mail: areu<at>free.fr

Site updated 08 May, 2017    Remarks and questions? areusite at free dot fr