99 Bottles – Mindful TDD Practise

In the last 2 meetings of XP Man, we’ve been mobbing to write a sudoku solver, as part of an exercise to explore the limits of TDD. As we discussed in our retro at the end of the second session, we’d struggled to make real progress. The reasons suggested for this were many and varied; possibly the format of such a large group who were unfamiliar with mobbing together, and a relatively short time-frame led to the code being pulled in many different directions.

My personal thoughts were that while the next step to take in TDD can lead to some disagreement and discussion (which test to write next, the simplest way to make it pass, when and what to refactor) our disagreement seemed to be more fundamental than that, and could even be interpreted as having different views on the what process of TDD is itself.

Earlier that week, in a separate group (our Book Club meeting) we had just finished reading Sandi Metz and Katrina Owen’s 99 Bottles of OOP, and had chosen to try a kata to mindfully practise what we had learnt. This struck me as being a very interesting contrast to our experience at XP Man. After pondering this for a while, I’ve decided to write this blog to capture our practise session. Moreover, I humbly hope to use it to illustrate how the XP Man session differed from TDD as I understand it.

Anyway, on with the illustration. We chose a classic kata for our book club experiment, the Roman Numerals Kata, in which we transform a string containing a value in Roman numerals into an integer. Even though this is a well known exercise, most of the members of the group hadn’t done it before, so our attempt was largely uninfluenced by past experience.

I’m not going to include the tests here to keep the amount of code under control. As you can imagine, they’re pretty straight-forward, passing in the Roman value and expecting the corresponding integer.

Our first tests converted I to 1, and we made the simplest possible implementation:

    private int roman(String roman) {
        return 1;
    }

When we added our test for II to 2, one of our group spotted that we could just return the length of the string…

    private int roman(String roman) {
        return roman.length();
    }

and the tests passed, including the next test for III being 3 (I’ll come back to this shortly).

Next we added a test for IV being 4, and made it pass with an explicit condition:

    private int roman(String roman) {
        if (roman.equals("IV")) {
            return 4;
        }
        return roman.length();
    }

And then we extended it with the case for V.

    private int roman(String roman) {
        if (roman.equals("IV")) {
            return 4;
        } else if (roman.equals("V")) {
            return 5;
        } else if (roman.equals("I") {
            return 1;
        }
        return roman.length();
    }

At this point, we made our first connection with the book, and decided to refactor the roman.length() for the I, II and III cases into explicit conditions.

    private int roman(String roman) {
        if (roman.equals("IV")) {
            return 4;
        } else if (roman.equals("V")) {
            return 5;
        } else if (roman.equals("I")) {
            return 1;
        } else if (roman.equals("II")) {
            return 2;
        } else if (roman.equals("III")) {
            return 3;
        }
        return roman.length();
    }

The book tells us to make our code more similar, to reveal the patterns therein and be able to spot the deeper abstractions. The two different ways of writing code (using length() and using explicit conditions) made it much harder to see that these were actually similar cases. Trying to introduce an abstraction too soon had actually made it harder to see what we were doing. Sandi Metz also mentions this in her blog The Wrong Abstraction where she says ‘Prefer duplication to the wrong abstraction’. This did leave us with the awkward default case of returning the length() that we knew was now not being used by the tests, but we thought we’d make a mental note of the fact and press on to see if the problem would resolve itself.

The book club mod didn’t feel quite ready to start finding abstractions just yet, so we decided to add a test for VI and add another condition.

    private int roman(String roman) {
        if (roman.equals("IV")) {
            return 4;
        } else if (roman.equals("V")) {
            return 5;
        } else if (roman.equals("I")) {
            return 1;
        } else if (roman.equals("II")) {
            return 2;
        } else if (roman.equals("III")) {
            return 3;
        } else if(roman.equals("VI")) {
            return 6;
        }
        return roman.length();
    }

With a bit of refactoring to remove the unnecessary else keywords, we were able to rearrange the code in numeric order.

	private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("II")) {
            return 2;
        }
        if (roman.equals("III")) {
            return 3;
        }
        if (roman.equals("IV")) {
            return 4;
        }
        if (roman.equals("V")) {
            return 5;
        }
        if (roman.equals("VI")) {
            return 6;
        }
        return roman.length();
    }

While we didn’t have tests for all cases, the book-clubbers recognised this as another idea from 99 Bottles. We felt that this was what the book refers to as a Shameless Green solution. By the book’s definition Shameless Green is ‘the solution which quickly reaches green while prioritizing understandability over changeability’ and ‘patiently accumulates concrete examples while awaiting insight into underlying abstractions’. A shameless green solution makes a good point to start looking for abstractions.

This makes the first point of contrast to what I remember being in the XP Man mob. Other people in the group my disagree, but to my mind I felt like we didn’t arrive at any point where we had a shameless green solution, one where we’d just tried to get the tests green without adding any particular design. At the most we had 2 or three cases in the code, and I remember at times we’d prematurely started to look for abstractions. I think it would have been useful to have a few more cases before we started to look for abstractions. To my mind, not having a shameless green solution with a reasonable number of cases made it hard to spot the patterns to base abstractions around.

So, back to the Roman numerals. From shameless green, we started looking for patterns. The I being 1 case seemed pretty atomic, but we spotted that the II being 2 was really more like II being 1 + 1, so we made it so.

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("II")) {
            return 1 + 1;
        }
        if (roman.equals("III")) {
            return 3;
        }
        if (roman.equals("IV")) {
            return 4;
        }
        if (roman.equals("V")) {
            return 5;
        }
        if (roman.equals("VI")) {
            return 6;
        }
        return roman.length();
    }

and then we generalised to all the cases:

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("II")) {
            return 1 + 1;
        }
        if (roman.equals("III")) {
            return 1 + 1 + 1;
        }
        if (roman.equals("IV")) {
            return -1 + 5;
        }
        if (roman.equals("V")) {
            return 5;
        }
        if (roman.equals("VI")) {
            return 5 + 1;
        }
        return roman.length();
    }

Another of the book-clubbers then suggested that each of the ones in 1 + 1 could be generated by the recursive calls, which the mob agreed with, and so produced:

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("II")) {
            return roman("I") + roman("I");
        }
        if (roman.equals("III")) {
            return 1 + 1 + 1;
        }
        if (roman.equals("IV")) {
            return -1 + 5;
        }
        if (roman.equals("V")) {
            return 5;
        }
        if (roman.equals("VI")) {
            return 5 + 1;
        }
        return roman.length();
    }

and that this could be done for all the multiple-numeral cases:

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("II")) {
            return roman("I") + roman("I");
        }
        if (roman.equals("III")) {
            return roman("I") + roman("I") + roman("I");
        }
        if (roman.equals("IV")) {
            return - roman("I") + roman("V");
        }
        if (roman.equals("V")) {
            return 5;
        }
        if (roman.equals("VI")) {
            return roman("V") + roman("I");
        }
        return roman.length();
    }

We then spotted from the code that the the I and V cases were similar and so moved them to the start of the function to make them stand apart from the multiple-numeral cases.

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("V")) {
            return 5;
        }

        if (roman.equals("II")) {
            return roman("I") + roman("I");
        }
        if (roman.equals("III")) {
            return roman("I") + roman("I") + roman("I");
        }
        if (roman.equals("IV")) {
            return - roman("I") + roman("V");
        }
        if (roman.equals("VI")) {
            return roman("V") + roman("I");
        }
        return roman.length();
    }

The next step was spotted by one of the junior members of the group, and involved making the code more similar again. The case for III was now made of 3 recursive calls, but could be changed to recursive calls for I and for II. This made the cases of II, III and VI more similar again, as could be seen from the similarity of the if statements. This was something else the book had told us to look out for as it hinted that there was an abstraction to be discovered.

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("V")) {
            return 5;
        }

        if (roman.equals("II")) {
            return roman("I") + roman("I");
        }
        if (roman.equals("III")) {
            return roman("I") + roman("II");
        }
        if (roman.equals("VI")) {
            return roman("V") + roman("I");
        }

        if (roman.equals("IV")) {
            return - roman("I") + roman("V");
        }
        return roman.length();
    }

We could see that the II, III and VI cases were similar, and could be solved by recursion, so we moved the still outstanding special case for IV higher up to leave the interesting cases last.

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("V")) {
            return 5;
        }

        if (roman.equals("IV")) {
            return - roman("I") + roman("V");
        }

        if (roman.equals("II")) {
            return roman("I") + roman("I");
        }
        if (roman.equals("III")) {
            return roman("I") + roman("II");
        }
        if (roman.equals("VI")) {
            return roman("V") + roman("I");
        }

        return roman.length();
    }

We could then generalise these cases to being the roman value of the first roman numeral added to the value of the remaining numerals. Note that this step also resolved the dangling case of the length() that we were worried about.

    private int roman(String roman) {
        if (roman.equals("I")) {
            return 1;
        }
        if (roman.equals("V")) {
            return 5;
        }

        if (roman.equals("IV")) {
            return - roman("I") + roman("V");
        }

        return roman(roman.substring(0, 1)) + roman(roman.substring(1));
    }

Again, this was something we remembered from the book. We were reducing duplication, but not just by finding identical characters in the code, but by finding duplicated concepts. Here the first part of the return statements, roman(“I”)/roman(“I”)/roman(“V”), represent the same concept the value of the first roman numeral even though they are not the exact same text.

However, while looking back at this for this blog, I think that maybe we made a slightly bigger leap in the last step than we should have. Maybe the excitement of discovering the abstraction made us rush ahead a little. I think we could have made a couple of smaller steps of making the code more similar before finally arriving pulling out the abstraction. As it happens, the bigger step worked and the tests passed, so all was OK this time.

At this point the book club mod had run out of time and so had to stop. We’d had an hour session, but had lost 20 minutes at the start trying to setup AV in a new room. What you see here had been achieved by a group of 5 in only 40 minutes. While we had not completed the kata, the book-clubbers felt like they had practised well and applied their learning, and that was valuable.

One thing worth noting is that since arriving at our Shameless Green solution, the book-clubbers did nothing other than refactor. That refactoring took the form of very small changes to the existing code, one at a time, where the tests always passed. This for me is the essence of Test Driven Design, i.e. the design of the code should emerge from the test cases alone. We did not go looking for a design to solve this problem, the design found us from the examples, just by us just paying attention to the small details (bike-shedding, as Kevin put it).

Another observation to be made is the cadence of TDD. If we naively follow the the TDD mantra of Red/Green/Refactor, it seems that these 3 stages should equate to roughly equal amounts of work, repeated in that order. In reality, getting to Shameless Green is mainly a process of Red/Green with only a little Refactoring. Once enough test cases are simply expressed in the code, a more lengthy period of Refactoring often ensues. This is borne out by the book. Chapter 1 introduces Shameless Green, and then chapter 2 circles back and explains how to drive it from the tests. The book has 4 further chapters that are almost all refactoring (apart from the final section of the final chapter that implements a new feature that the refactorings have made easy).

This is the final point of contrast I’d like to make with the XP Man experience. On more than one occasion, the XP Man mob started writing speculative code. For me this did not feel like TDD. I was expecting code to only be written to make a failing test pass, and other than that for code to change via baby-step refactorings.

Being an exercise to discover the limits of TDD, the sudoku solver still interests me and I’d like to see a solution generated via TDD (if possible!). Maybe we’ll undertake it with the book club mob.

One final message to the XP Man folks in particular. Hopefully this illustrates my understanding of TDD. If anything is unclear I’d be happy to answer questions. Moreover, if anybody has a different view on the process of TDD, then I’d be very happy to hear them set out in blog form or otherwise.

1 thought on “99 Bottles – Mindful TDD Practise

  1. andylongshaw

    I like this post, thanks for sharing your experiences with the book club. This approach is familiar from the workshop I mentioned with Sandi Metz and Matt Wynne a few years ago which was based on 99 bottles of beer. I don’t think I have a fundamentally different view of TDD but I wonder if the differences in approach we are seeing relate to the use of TDD as a way of exploring the domain. If you are in an unfamiliar domain then TDD gives you a good way of exploring the space and when you are doing that you absolutely do not want to leap to conclusions and create early abstractions that may be wrong. In that sense the “shameless green” approach keeps you honest. One of the things that I recall Steve Freeman saying about TDD is the way it stops you from prematurely applying one of the possible solutions you have “in your toolbox” and keeps you exploring (http://higherorder.wpengine.com/?p=176) so that “Later on, when you’ve gathered some empirical evidence, you can see if you were right and move the code in that direction.” The principle of not prematurely applying a possible solution to a problem space you don’t yet know well enough is a good on.

    I wonder if our problem is that we do actually know the problem space quite well. I know how I can solve a sudoku without a computer. I know that there are places where I could go to find techniques to solve more complex sudoku problems that my current set of simplistic techniques can’t crack. The question then is why I would want to use TDD to find a solution in a problem space where I already have a solution. If I want to create an algorithmic version of this then I can take a test-first approach to implementing a solution *that I know works already*. And maybe this is the bit I (and others) am missing. The objective of the exercise is not to actually solve sudoku, it is to see if “by following simple refactorings as part of a TDD cycle” you can end up with a solution. The problem here I think harks back to “when you’ve gathered some empirical evidence” because it feels like you have to effectively forget what you know about sudoku solving techniques and intentionally adopt a beginner’s mind. I’d argue that this is slightly different from most of the situations that you come across when creating business software as in this case you are intentionally trying to avoid implementing something that you know would work in order to prove the efficacy of the method. I wonder if the problem is also to do with the familiarity of the subject matter rather than the fact that it is a relatively complex and algorithmic problem which may have been part of the initial challenge that underlay the set of blog posts from Ron Jeffries.

    Would be interested to see where you get to with this or if we can take another run at it in XP Manchester.

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s