Bowling Game Experiment

1/27/01 - Jack Harich - Go Home


This experiment occurred due to an email exchange with Robert C. Martin (Bob) on the comp.object newsgroup. A long thread was about XP versus various OOAD practices, notably the value of design before code. Bob emailed me a pdf file containing the manuscript chapter "A Programming Episode", 35 pages long. After much work, I sent the following in a message 1/26/01 to the comp.object newsgroup. Bolding was added later.


Original EMail Message

...(snip)... The chapter is a dialog, with code and models, of Bob and another programmer doing pair programming, XP style of course. The practice project is a bowling game scorer. The chapter is very well written - easy to read, plenty of code examples.

Bob had made this interesting comment to me about the chapter:

"If you look back, you'll see that we did, in fact, start with a small analysis and design. We even cobbled together a small UML diagram. You'll also notice that it did no good.

Indeed, this is an experiment I have run many many times. I propose the bowling problem to groups. They come up with designs that have many inteconnected objects. Then we proceed to design the problem test-first, and come to the minimal solution at the end of the document that looks *nothing* like the original designs.

It's always an eye-opener."

This implies that the process I use, and have taught to others, is unproductive. So using my process (a typical though very lightweight OOAD one called the Mini Process) I read just the first two pages to get requirements, and proceeded using my process. Visio was used to prepare a Nutshell Vision, Rough Requirements, two stepped out Use Cases, and a model with six classes. These were Scoreboard, Game, Frame, Console, League and Team. An Implementation Plan was drawn up with 2 iterations. Two test designs were done, one for each iteration.

Then I proceeded with iteration 1, doing the Frame class first. This took 3 hours. After that, project assessment showed the rest to be very straightforward, and so possibly not worth my time.

Then to see if I was way off base somehow, I read the rest of the chapter. Extremely interesting. My, my, my, how our process styles differ. :-)

Then I decided to finish iteration 1, so I did the Game class and the GameTest in 45 minutes, then debugged in 34 minutes and was done. On a larger system I would have done more testing as I went, but on trivial systems I find testing at the end to be more time efficient, because it doesn't distract from architecture, design and implementation. I sometimes go for days (and once weeks) of design and implementation with no testing, and it's never been a problem.

How did the model change during my implementation? In the Frame class, 2 methods were added, 3 renamed, and one removed, out of 13 method total. This is very typical. There were no changes to the 5 Game model methods. The other classes were not used at all, because it seems the requirements on page one were never implemented in the chapter sample, namely "keeps track of a bowling league, ...determine the ranks of the teams, determine the winners and losers of each weekly match". Perhaps this is in a later chapter. Designing these took considerable time and requirements guesses. In summary, I did not encounter "looks *nothing* like the original designs" using my process. The final product was identical to my model, except for the method changes noted. Plus, at the end I have high quality artifacts, including a roadmap (model) of the code. The chapter example produced no artifacts and the code was uncommented, while my code is well commented and blocked, per standards. I realize the chapter was simplified.

Here's some links to the process used: (this was not in the email, but an offer to provide process links was)

Introduction to the Mini Process - Digest this first.
The full Mini Process Grid - Showing additional elements you may use.
The improved version, Mini Process 2 - This has some great improvements.
The beginnings of Mini Process 3 - Be sure to see the Basic OOAD Process.

My strong conclusion, based on this emperical evidence and my experience, is that the process presented in the chapter is oriented towards developers who have weak OOAD skills. Whooops, I realize this is controversial. But think about it. The literature has reported that over half of all shops trying to adopt OO failed to do so. About 70% of American shops are at Level One on the CMM. From this and my own observations, I'd estimate that 70% or more of American developers have weak OOAD skills. Until XP this was a vast unfilled process void, because RUP, CMM, ISO, and many others were usually too heavyweight and/or required too much skill. XP is terrific for the average developer, because it hits the 70% sweet spot. I'm a strong proponent of XP, if taken with a few grains of salt. :-)

A further comment is the technique of "nearly no predesign" is not scalable. But that is another thread, and another day.... All in all, it's a case of "match the hatch", as the fly fisherman say.... :-)

Thanks to Bob and all,

Jack Harich


Artifacts

Visio was used to prepare the following one page of artifacts. I find that "One Page Style Management" is preferred whenever possible. For a larger project I'd uses html to capture the text. Bear in mind this is not fully comprehendible without an explaination from the author and familarity with the process.


Code

Note that all classes started from a standard skeleton. Note the high quality of this code compared to that in the chapter. See simple standards used.

package jsl.bowling;

/**
* This class represents a single frame in a standard 10 pin
* bowling game scoreboard.
*
* @author Jack Harich
*/
public class Frame {

//---------- Public Fields ---------------------------------------
public static final int NONE   = -1;

//---------- Internal Fields -------------------------------------
protected int number;
// There are all equal to NONE or 0 to 10.
protected int throw1 = NONE; 
protected int throw2 = NONE;
protected int throw3 = NONE; // For use in last frame only

protected Frame previousFrame; // Null if frame 1
protected Frame nextFrame;     // Null if frame 10

//---------- Initialization --------------------------------------
/**
* Creates a new instance. 
* @param number     must be from 1 to 10. Note this is one based.
*/
public Frame(int number) {
    if (number < 1 || number > 10) throw new IllegalArgumentException
        ("The frame number must be from 1 to 10.");
    this.number        = number;
}
//---------- Public Methods --------------------------------------
/**
* Sets the previous Frame. This must have a number one less that this
* frame. The first frame has no previous frame.
*/
public void setPreviousFrame(Frame frame) {
    if (number == 1) {
        if (frame != null) {
            throw new IllegalArgumentException
            ("The first frame has no previous frame.");
        } else {
            return;
        }
    }
    if (number - 1 != frame.getNumber()) throw new IllegalArgumentException
        ("My number is " + number + " but the previous frame number is " +
        frame.getNumber());
    previousFrame = frame;
}
/**
* Sets the next Frame. This must have a number one more that this
* frame. The last frame has no next frame.
*/
public void setNextFrame(Frame frame) {
    if (number == 10) {
        if (frame != null) {
            throw new IllegalArgumentException
            ("The last frame has no next frame.");
        } else {
            return;
        }
    }
    if (number + 1 != frame.getNumber()) throw new IllegalArgumentException
        ("My number is " + number + " but the next frame number is " +
        frame.getNumber());
    nextFrame = frame;
}
/**
* Returns the frame number, which is from 1 to 10.
*/
public int getNumber() {
    return number;
}
/**
* Resets the throw data to NONE, which
* returns the instance to its post constructor state for reuse.
*/
public void clear() {
    throw1 = NONE; 
    throw2 = NONE; 
    throw3 = NONE;
}
/**
* Adds a bowling ball "throw" to the frame. Do not call this method if
* the frame is "done".
* @param numberPins  must be from 0 to 10.
*/
public void addThrow(int numberPins) {
    if (isDone()) throw new IllegalStateException
        ("Sorry, frame number " + number + " is done.");
    if (numberPins < 0 || numberPins > 10) throw new IllegalArgumentException
        ("Number of pins must be from 0 to 10.");
    if (throw1 == NONE) {
        throw1 = numberPins;        
    } else if (throw2 == NONE) {
        throw2 = numberPins;
    } else {
        throw3 = numberPins;
    }
}
/**
* Determines whether all throws have been made for this frame. In the case
* of the last frame, if the first throw was a strike then the frame is not
* done until 3 throws have been made for the frame.
*/
public boolean isDone() {
    if (number < 10) {
        if (throw1 == 10) {
            return true;
        } else {
            return throw2 != NONE;
        }
    } else {
        // Last frame
        if (throw1 == 10) {
            return throw3 != NONE;
        } else if (throw1 + throw2 == 10) {
            return throw3 != NONE;;
        } else {
            return throw2 != NONE;
        }
    }
}
/**
* Returns the number of pins knocked down in throw 1 or the NONE constant.
*/
public int getThrow1() {
    return throw1;
}
/**
* Returns the number of pins knocked down in throw 2 or the NONE constant.
*/
public int getThrow2() {
    return throw2;
}
/**
* Returns the number of pins knocked down in throw 3 or the NONE constant.
* For all but frame 10 this is irrelevant and always equal to NONE.
* This is because a third throw is allowed on frame 10 in case the first
* throw in the frame is a strike.
*/
public int getThrow3() {
    return throw3;
}
/**
* Returns the total score so far or -1 if not yet known. The total
* is not yet known if the frame is not yet complete or if a strike
* or spare is in this frame and the subsequent throws are not yet
* made.
*/
public int getTotalScore() {
    if (! isDone()) return -1; // Not yet known
    int myScore = 0;
    // Normal points
    myScore += (throw1 == NONE ? 0 : throw1);
    myScore += (throw2 == NONE ? 0 : throw2);
    myScore += (throw3 == NONE ? 0 : throw3);
    // Extra points
    if (throw1 == 10) { // Strike
        if (number < 10) {
            int extraPoints = nextFrame.getStrikeExtraPoints();
            if (extraPoints == -1) return -1;
            myScore += extraPoints;
        } else {
            // Do nothing, throw2,3 already added to myScore
        }
    } else if (throw1 + throw2 == 10) { // Spare
        if (number < 10) {
            int extraPoints = nextFrame.getSpareExtraPoints();
            if (extraPoints == -1) return -1;
            myScore += extraPoints;
        } else {
            // Do nothing, throw3 already added to myScore
        }
    }
    // Convert to total running score for all frames up to this one
    if (previousFrame != null) myScore += previousFrame.getTotalScore();
    // Done
    //print(".getTotalScore() - total = " + number + " - " + myScore);
    return myScore;
}
// The following were discovered as needed, not on first model
/**
* Returns the number of pins down in the next two throws, for use
* in adding to 10 to calculate a strike's score. Returns -1 if
* not yet known.
*/
public int getStrikeExtraPoints() {
    if (throw1 == NONE) {
        return -1;
    } else if (throw1 == 10) { // Two strikes in a row
        if (number == 10) {
            return (throw2 == NONE ? -1 : throw1 + throw2);
        } else {
            int nextThrow = nextFrame.getThrow1();
            return (nextThrow == NONE ? -1 : 10 + nextThrow);
        }
    } else {
        return throw1 + throw2;
    }
}
/**
* Returns the number of pins down in the next single throw, for use
* in adding to 10 to calculate a spare's score. Returns -1 if not
* yet known.
*/
public int getSpareExtraPoints() {
    return (throw1 == NONE ? -1 : throw1);
}
//---------- Standard --------------------------------------------
private static void print(String text) {
    System.out.println("Frame" + text);
}

} // End class

package jsl.bowling;

/**
* This is a single bowling game. The activity and scores are
* available by accessing the Frame desired. Once isDone() goes
* true, more throws cannot be added. A Game instance can be reused
* via clear().
*
* @author Jack Harich
*/
public class Game {

//---------- Internal Fields -------------------------------------
protected static final int NUMBER_FRAMES = 10;
protected Frame[] frames = new Frame[NUMBER_FRAMES];
protected int     currentFrameIndex = 1;

//---------- Initialization --------------------------------------
public Game() {
    // Init frames array
    for (int i = NUMBER_FRAMES - 1; i >= 0; i--) {
        Frame frame = new Frame(i + 1);
        frames[i] = frame;
    }
    // Set previous and next 
    for (int i = NUMBER_FRAMES - 1; i >= 0; i--) {
        Frame frame = frames[i];
        if (i != 0) frame.setPreviousFrame(frames[i - 1]);
        if (i != NUMBER_FRAMES - 1) frame.setNextFrame(frames[i + 1]);
    }    
}
//---------- Public Methods --------------------------------------
/**
* Clears all activity, which returns the Game to its post
* constructor state for reuse.
*/
public void clear() {
    for (int i = NUMBER_FRAMES - 1; i >= 0; i--) {
        frames[i].clear();
    }   
    currentFrameIndex = 1;
}
/**
* Adds a bowling ball throw given the number of pins knocked down
* in the throw, which must be from 0 to 10. 
*/
public void addThrow(int pinsDown) {
    if (isDone()) throw new IllegalStateException
        ("Cannot add throw since game is done.");
    Frame frame = getFrame(currentFrameIndex);
    frame.addThrow(pinsDown);
    if (frame.isDone()) currentFrameIndex++;
}
/**
* Determines whether the game is done.
*/
public boolean isDone() {
    return currentFrameIndex > NUMBER_FRAMES;
}
/**
* Returns the current total score or -1 if not done.
*/
public int getTotalScore() {
    if (isDone()) {
        return getFrame(NUMBER_FRAMES).getTotalScore();
    } else {
        return -1;
    }
}
/**
* Returns the frame for the number, which must be from 1 to 10.
*/
public Frame getFrame(int number) {
    return frames[number - 1];
}
//---------- Standard --------------------------------------------
private static void print(String text) {
    System.out.println("Game" + text);
}

} // End class

package jsl.bowling;

/**
* This is the unit test for the Game class. In a real app this
* would be in the Jester test framework.
*
* Additional testing was not done, because this is a practice
* project and I felt that would not cause more learning. As the
* GameTest Design shows, things like 2 strikes in a row and 
* various permutations of the last frame are needed.
*
* @author Jack Harich
*/
public class GameTest {

//---------- Internal Fields -------------------------------------
protected Game game = new Game();

//---------- Initialization --------------------------------------
public static void main(String arg[]) {
    new GameTest().runTest();
}
//---------- Public Methods --------------------------------------
public void runTest() {
    game.clear();
    runSampleTest();
}
public void runSampleTest() {
    // Frame 1
    game.addThrow(1);
    game.addThrow(4);
    // Frame 2
    game.addThrow(4);
    game.addThrow(5);
    // Frame 3
    game.addThrow(6);
    game.addThrow(4); // Spare
    // Frame 4
    game.addThrow(5);
    game.addThrow(5); // Spare
    // Frame 5
    game.addThrow(10); // Strike
    // Frame 6
    game.addThrow(0);
    game.addThrow(1);
    // Frame 7
    game.addThrow(7);
    game.addThrow(3); // Spare
    // Frame 8
    game.addThrow(6);
    game.addThrow(4); // Spare
    // Frame 9
    game.addThrow(10); // Strike
    // Frame 10
    game.addThrow(2);
    game.addThrow(8); // Spare
    game.addThrow(6); 
    
    // Check individual frame scores
    assertFrameScore(1, 5);
    assertFrameScore(2, 14);
    assertFrameScore(3, 29);
    assertFrameScore(4, 49);    
    assertFrameScore(5, 60);
    assertFrameScore(6, 61);
    assertFrameScore(7, 77);
    assertFrameScore(8, 97);
    assertFrameScore(9, 117);
    assertFrameScore(10, 133);
    // Check final score of game
    print("- Final score = " + game.getTotalScore());
    assert("Final score", 133 == game.getTotalScore());
}
//---------- Protected Methods -----------------------------------
protected void assertFrameScore(int frameNumber, int score) {
    assert("Frame " + frameNumber + " score = " + score,
        score == game.getFrame(frameNumber).getTotalScore());
}
protected void assert(String message, boolean assertion) {
    if (assertion == false) throw new RuntimeException
        ("Assertion failure, message: '" + message + "'.");
}
//---------- Standard --------------------------------------------
private static void print(String text) {
    System.out.println("GameTest" + text);
}

} // End class

Thread Replies

Here's the interesting replies in the thread so far with my replies. I'm always learning. :-)

Nick Thurn wrote:
> Hi Jack,
> 
> I haven't gone through your experiment but your comments indicate
> we may have similar approaches. The issue you raise of XP being
> suitable for "those with weak OOAD skills" (ie the "average programmer")
> has been my hope (although I didn't put it in quite the same way).

It's been my hope too, but the explosive popularity of XP has caused me
to wonder if I had some blind spots, so I did this experiement.

I don't see all this as OOAD versus XP, but rather a discussion of
discrete practices that are part of many processes. For example, use
cases and modeling are practices. The key issue, I suspect, is that
practices must be done _well_ to be effective. If done poorly they have
negative effects. So "skillful" means that one knows how to use a
practice well, not just use it.

A good example of this is "process step exit criteria". Most developers
cannot define this term without help. Let's apply it to the design step.
How does one know the design is done and reasonably sufficient? By
applying exit criteria. In the case of a model one looks upstream and
validates the model supports all uses cases, system traits and risks.
Only if the model passes this check are you done. The chief benefits of
exit criteria are avoiding too little and too much work, and achieving
at least a sufficient minimum of usefulness.

The "A Programming Episode" chapter had a UML model, but guess what?
There was no exit criteria step. The poor programmer proceeded to try to
implement the model, and of course it failed to be useful. Also the
model was very inadequate, since it had no methods. All is all, a
textbook example of how _not_ to do the design step.

 
> The XPer's however seem reluctant to embrace this idea, possibly
> because it isn't what their audience want to hear. This is a shame
> if it is the case, the Dilbert Zone needs all the help it can get.

True. They seem to feel threatened, and so raise a great hue and cry.
However, some are quite level headed, and see all sides of the issue,
sometimes more that I see. :-) These are the one I enjoy learning with
the most.

 
> In the case where you *do* have skilled OOAD folks is XP going
> to be an improvement, a drag anchor or equally productive?

Hmmmmm. good question. I'll pass, because I haven't seen any data here.

But based on no data, my guess is that once the mature developer has
absorbed XP, they will use its many fine practices to "match the hatch"
of each project, team or company. Every process has some practice jewels
to offer.

http://www.martinfowler.com/articles/xpVariation.html has a section on
"XP and Self-Adaptivity" where perhaps Kent Beck answers your question
best himself. He says:

"Level 1 is where you are doing all of the practices exactly by the book 
 Level 2 is where you have adapted XP for your local conditions 
 Level 3 is where you don't care whether you're doing XP or not"

This is deep wisdom. I've memorized this thought....

Jack

Jack Harich wrote:
> Bob had made this interesting comment to me about the chapter:
>
> "If you look back, you'll see that we did, in fact, start with a small
> analysis and design.  We even cobbled together a small UML diagram.
> You'll also notice that it did no good.

Peter Douglass replied:

   Everyone has anecdotes.  I won't say that Bob's experience is not
valid.  However, I have had an experience which goes the other way.

   In the field I work in, testing costs rival or exceed development
costs.  I had read of developers who wrote (correct) code without ever
compiling once, and thought I would give it a try, it being a point of
honor to write code with a low defect count.  I used *design by
contract* (at least as far as I understand it).  I was 50% through my
schedule and had still not completed my design.  I began to worry, and
consulted a friend in my department.  Do you think I should quit
designing and start coding?  He suggested I should hold my course.  I
finished design 3/4ths through the schedule.  When I coded, I
discovered one defect in my design.  In unit test, I discovered one
defect in my code.  When the code was handed to Q/A, only one
additional defect was discovered (coding).

  Is pre-design a waste?  Perhaps it is for some people.  Perhaps in
some situations.  For me it seems very valuable.  What would be nice
would be studies of the conditions (type of task, personality of
programmer, business objectives etc.) which make one strategy superior
to another.

Jack Harich wrote:

> Yes. I like what you said and how you said it. Flexibility is so
> important.
>
> Re: "Does all of your upfront design still pay off?" - A well run
> project recognizes risk and plans for it. Rarely will _all_ upfront
> design pay off, but it must be done to resolve risks. Invariably some up
> front decisions are wrong, and adjustment is made as the arrow flies....

Kent Beck replied:

Up-front design is not the only risk resolution strategy. Prototyping is
another, but it comes with fearsome risks. Organic design a la XP is also a
risk resolution strategy.

Jack Harich replied:

Thanks, Kent. I agree upfront design is not the only risk resolution
strategy.

As I see it, in some situations prototyping has "fearsome risks" and in
others it has very low risk and is indeed preferred to actually resolve
risk. For example GUI protoptypes are widely used in the industry. For
example sometimes a product prototype must be done to address high level
risks, so architectural spike work is done. For example if a tough
algorithm needs to be researched and worked out, sometimes a prototype
spike is best first done. For example if third party technology is used
for a project, and is unfamiliar, often prototype work is done to become
familiar with it or to make a buy/no-buy decision.

I like the phrase "Organic design a la XP is also a risk resolution
strategy." If a team (or developer) is rated as low in OOAD skills, or a
team seems to not deeply grasp a domain, or the customer is giving weak
requirements, etc, then organic design may be a good risk reduction
strategy. Early emergence of system behavior due to early coding becomes
a useful feedback loop, steering the team away from the rocky shoals of
risk. But another team might use up-front design, and so have a map of
the sea, and so avoid the shoals that way. 

This is not an either or choice, but a spectrum with no predesign on one
end and 100% on the other. For example we commonly hear that one should
stop when about 80% of requirements are gathered, because diminishing
returns occur after that. The same is true for design up front - Stop at
the point of diminishing returns, and iteratively and organically flesh
out what's missing. (but you know this :-)

See
http://www.mindspring.com/~happyjac/site/info/process/MiniProcess2.html
and scroll down to "List of Common Project Risks" for further risk
examples. (This link has been changed.)

BTW, I spent Christmas in Chico, California, visiting my dad. I took
daily walks around town and hiked up Bidwell Park with its Chico Creek,
sometimes past Bear Hole with all those amazing and beautiful jagged
rocks. Loved the buttes and the majestic canyon. Saw magnificant wedges
of honking geese overhead, on their way to or from the rice paddies.
Drove up to see the huge Oroville Dam. Ahhhhh, a great vacation. Bring
back memories? :-)