A response to the SWE101 attendees who tried to solve the TDD problem without TDD
This weekend we did some live TDD at the Software Engineering 101 event and broadcast it over LiveMeeting, which was a lot of fun. However, some people have posted solutions (here and here – see first comment) on how they solved the Greed scoring problem without using TDD.
I think you missed the point. The point was to help you learn the TDD thought process and how to put TDD into practice. There are lots of benefits to TDD, and I’ll use these non-TDD examples to illustrate.
How do you know your code works?
Those of you who solved the problem without TDD probably did some sort of manual testing in order to prove that your code is working. You could probably do that with a simple example like the one we had (there were only a few scoring rules). But what happens as we add rules (and in real life we’re always adding rules) and you have to manually test 16 rules? For the record, I took the solution posted here and ran my tests against it.
Tests are documentation
Tests document what the code is supposed to do. Since we wrote our test methods as sentences that read like English, you can figure out the rules just from reading our tests (as you can see in the image above). This is not documentation in Word format which becomes stale, this is living, breathing executable documentation of what the code is supposed to do.
Readability is important
I feel that the solution that we ended up with was very readable. Here is our implementation code:
public class GreedScorer
{
public double Score(params Die[] dice)
{
var score = 0;
score += ScoreASetOfThreeOnes(dice);
score += ScoreASetOfThreeTwos(dice);
score += ScoreASetOfThreeThrees(dice);
score += ScoreASetOfThreeFours(dice);
score += ScoreASetOfThreeFives(dice);
score += ScoreASetOfThreeSixes(dice);
score += ScoreEachOneThatIsNotAPartOfASetOfThree(dice);
score += ScoreEachFiveThatIsNotAPartOfASetOfThree(dice);
return score;
}
private int ScoreASetOfThreeOnes(Die[] dice)
{
if (dice.Count(die => die.Value == 1) >= 3)
return 1000;
return 0;
}
private int ScoreASetOfThreeTwos(Die[] dice)
{
if (dice.Count(die => die.Value == 2) >= 3)
return 200;
return 0;
}
private int ScoreASetOfThreeThrees(Die[] dice)
{
if (dice.Count(die => die.Value == 3) >= 3)
return 300;
return 0;
}
private int ScoreASetOfThreeFours(Die[] dice)
{
if (dice.Count(die => die.Value == 4) >= 3)
return 400;
return 0;
}
private int ScoreASetOfThreeFives(Die[] dice)
{
if (dice.Count(die => die.Value == 5) >= 3)
return 500;
return 0;
}
private int ScoreASetOfThreeSixes(Die[] dice)
{
if (dice.Count(die => die.Value == 6) >= 3)
return 600;
return 0;
}
private int ScoreEachOneThatIsNotAPartOfASetOfThree(Die[] dice)
{
if (dice.Count(die => die.Value == 1) < 3)
return (dice.Count(die => die.Value == 1) * 100);
if (dice.Count(die => die.Value == 1) > 3)
return ((dice.Count(die => die.Value == 1) - 3) * 100);
return 0;
}
private int ScoreEachFiveThatIsNotAPartOfASetOfThree(Die[] dice)
{
if (dice.Count(die => die.Value == 5) < 3)
return (dice.Count(die => die.Value == 5) * 50);
if (dice.Count(die => die.Value == 5) > 3)
return ((dice.Count(die => die.Value == 5) - 3) * 50);
return 0;
}
}
Look how readable our code is. Read the score method. Notice how it tells you exactly what it’s doing. Contrast that with one of the other solutions:
static int Score(int[] numbers)
{
int valueToReturn = 0;
for (int numberIndex = 0; numberIndex < numbers.Length; numberIndex++)
{
switch (numbers[numberIndex])
{
case 1:
if (
(numberIndex + 1 < numbers.Length && numbers[numberIndex + 1] == 1) &&
(numberIndex + 2 < numbers.Length && numbers[numberIndex + 2] == 1)
)
{
valueToReturn += 1000;
numberIndex += 2;
}
else
{
valueToReturn += 100;
}
break;
case 5:
if (
(numberIndex + 1 < numbers.Length && numbers[numberIndex + 1] == 5) &&
(numberIndex + 2 < numbers.Length && numbers[numberIndex + 2] == 5)
)
{
valueToReturn += 500;
numberIndex += 2;
}
else
{
valueToReturn += 50;
}
break;
default:
if (
(numberIndex + 1 < numbers.Length && numbers[numberIndex + 1] == numbers[numberIndex]) &&
(numberIndex + 2 < numbers.Length && numbers[numberIndex + 2] == numbers[numberIndex])
)
{
valueToReturn += 100 * numbers[numberIndex];
numberIndex += 2;
}
break;
}
}
return valueToReturn;
}
To me, this code is not very readable. If you had to implement a new rule in this method, it would be hard to do (plus you have no tests to tell you that you broke an existing rule).
The point of this exercise was not to find a clever way to solve a brain teaser. If you want to do a brain teaser, try and write code that will output a Fibonacci sequence using one LINQ expression. That's the kind of geek stuff that you might do at night for fun. But when you're writing real code, we care about things like readability and maintainability. When you're writing implementation code, use common language and write methods whose names tell you what they do.
TDD leads to well-designed code
As we were writing our first test, our test told us that we needed some class that would score the Greed game. So we named it GreedScorer, which does exactly what the name intends. This class will most likely end up following the single responsibility principle because we gave it a very specific name.
But you guys took so long!
We weren't trying to finish as fast as possible. We were not doing a coding competition. We were practicing and learning the TDD mindset and trying to explain things as we went. As you get better at TDD, it becomes more natural and you learn tricks that help you go faster (for example, sometimes writing a whole bunch of tests, watching them all fail, then making them all go pass is faster than just writing one test at a time and making them pass one at a time).
The bottom line is that we came out with some good, readable, maintainable code (with props to Sirena who helped write it). We were able to prove that our code is working. We don't have any code that we don't need. We could easily respond to change and implementing new scoring rules would be pretty easy. Implementing the Score method was fairly easy because we were building it incrementally (solving lots of little problems is easier than trying to solve a big problem all at once). This is why we do TDD!
I totally agree. I have seen code maintained for years that there has been so much functionality put in a single method that the developer put regions inside the method. If your method is more than 20 lines long, it is a smell that there might be something wrong with your design.
Switch statements are ugly in general. If you are coding up a switch statement, you might want to stop and ask yourself if there is a better way of doing this. In my experience, there always is.
In the first example I’m on the fence on whether I would abstract the logic of one of the six “ScoreASetOfThree” methods into a single method or leave it as is. It’s very readable as it stands, but I see a nagging opportunity remove some repetitiveness.
Thanks for sharing! Did you happen to record the event? I would like to see it.
@Chuck,
We did record it. Hopefully it worked. :) I’ll blog about it if it gets posted somewhere.
Jon
yes . the code above which is written without TDD is confusing and needs more concentration
Jon,
Please take a look at the following code metrics (static analysis) produced by Visual Studio Team System 2008 Development Edition and compare:
Type: GreedScorer
Maintainability Index: 66
Cyclomatic Complexity: 48
Depth of Inheritance: 1
Class Coupling: 3
Lines of Code: 62
Member: Score(int[]) : int
Maintainability Index: 50
Cyclomatic Complexity: 16
Class Coupling: 0
Lines of Code: 21
Although, your implementation has a maintainability index a bit higher than mine (16 points up), your implementation has a cyclomatic complexity 3x higher than mine (why complain about my fors, switches and ifs, as some of your blog followers did in the comments, when my implementation has a much much lower cyclomatic complexity), your implementation has Depth of Inheritance and Class Coupling, whereas mine does not have. Moreover, you implementation has 3x the lines of code mine has.
I haven’t run a dynamic code analysis (profiling/instrumentation) to compare implementations, but just reviewing your code it becomes clear it has performance implications. It iterates through same array so many times, whereas mine iterates through the array items just once.
If I had commented my code, maybe it would be friendlier to some, but come on people, I implemented it in less than 5 minutes. How long did it take you to implement yours? As far as I remember, a whole afternoon. Don’t be so rude on me, please.
Your presentation about TDD was great, when looking at the WHAT perspective (how to create the tests, what tests to undergo, etc., etc.), but when it came to the HOW part, it seems you, people, got lost and implemented something not that simple, just to satisfy the requirements your tests carried on. While the #swe101 was live (underway), I asked you, people, to use and adapt my implementation (the HOW perspective), so that to avoid wasting so much time on it and to check my implementation (the HOW part) was conformant to the tests you created (the WHAT perspective), but no one took it into account.
If TDD was to sacrifice performance, increase complexity, depth, coupling and lines of code to write, I wouldn’t jump in for sure. Fortunately, it is not, as far as I am concerned about. Just an advice, pay much much more attention to the “WHAT perspective” next time and make things as simple as possible. I may not know much about the WHAT, but I know a lot of the HOW, since I review, refactor, fine tune and try the simplest wherever possible.
I don’t want start a war here (we have a lot out there already) and I won’t go any further on this. Just needed to come up and defend my code. That’s what I do for living for long.
Thanks and regards,
Luciano Evaristo Guerche (Gorše)
Taboão da Serra, SP, Brazil
Just in case you get the full text response truncated on comments: http://www.google.com/reader/item/tag:google.com,2005:reader/item/4f277f641a373cb1
@GuercheLE:
If I open Jon’s code, I can read it like text and understand EXACTLY what it’s intended to do and pretty clearly how it’s being done. I don’t mean to be insulting, but your implementation is ugly. It’s difficult to read, and, despite what your metric generator told you, has more indentions than Jon’s. That’s more telling of the complexity than a metric spit out by a VS tool. Metrics are a rule-of-thumb guideline, at best. At some point, it comes down to using your head.
The point in this workshop was not speed of implementation or (premature) optimization of the code. It was a learning excercise. You are kidding yourself if you think the people running this workshop couldn’t have knocked out that Greed engine in no time with a full suite of unit tests in full TDD fashion. The idea was to take the time to focus on how it is done for the people who don’t yet understand how to do TDD on a non-trivial task front-to-back.
But the most important metric is correctness of implementation. The whole thing is bunk when you come down to the fact that your code isn’t correct. By ignoring the testing and just slinging code, you left it open for bugs. What if you send yours {1,5,1,5,1}? You return 400. The correct response is 1100. It’s because you assume the sets of 3 are in runs, when they can be separated.
Don’t take it personally. Egoless programming is even more important than TDD. You are not defined by your code.
@Jon
Can’t wait for the recording. I hope to spread it around the office. Finally getting some buy-in on TDD around here, but I think it’s at the “but it will be too hard” phase for some.
@Luciano,
I think readability and maintainability are very important, and they aren’t necessarily measured by metrics. If by adding some extra if statements and methods I can make my code easier to read, then I think that’s a good thing. Let’s say that we added more rules to the Greed game, and someone other than you and I had to modify the code we had written (this happens all the time in real life). How hard would it be for that person to get the job done?
Like you said, your code probably is faster, but in this case, performance wasn’t really a consideration. Certainly if it were an issue, I might have to refactor something. But I’m guessing that the difference in performance is negligible anyway.
Again, the goal of the TDD session was not to see how fast we could solve the problem. We could’ve gone faster if we weren’t trying to demonstrate how to do TDD (which was the main goal of the whole thing).
@Andy,
The written requirements mentioned “sets of three X dice faces…” not “occurrences of X dice face”. Based on that I supposed items should be contiguous to be counted as a set. If I had supposed they didn’t need to be contiguous I’d have devised the following implementation instead:
private static int[] setWeights = new int[] { 0, 1000, 200, 300, 400, 500, 600, 700, 800, 900 };
private static int[] diceWeights = new int[] { 0, 100, 0, 0, 0, 50, 0, 0, 0, 0 };
static int Score(int[] diceValues)
{
return (
(
from item in diceValues
group item by item into dice
orderby dice.Key
select new
{
Dice = dice.Key,
Score = (dice.Count() / 3 * setWeights[dice.Key]) +
(dice.Count() % 3 * diceWeights[dice.Key])
}
).Sum(item => item.Score)
);
}
Code in the comments seems ugly, so I posted it to http://codepaste.net/srcjuh also. I also sending below the code metrics Visual Studio Team System generates about this new implementation.
Member: Score(int[]) : int
Maintainability Index: 63
Cyclomatic Complexity: 9
Class Coupling: 3
Lines of Code: 6
In case no one else noted there might be only seven weights (max number of faces + the zero index), so I changed private members to:
private static int[] setWeights = new int[] { 0, 1000, 200, 300, 400, 500, 600 };
private static int[] diceWeights = new int[] { 0, 100, 0, 0, 0, 50, 0 };
Yesterday I had an interesting conversation with my son, a Java coder, over this blog. We compared the programs of JK and LEG(G), above. He found Jon’s program far easier to read, and hearing him comment from his perspective helped me appreciate what he likes about it. The most obvious thing was how JK’s code fit his sense of normalcy, knowing Java but not C#. In contrast, LEG(G)’s code seemed impenetrable given his lack of C# knowledge. I, too, am ignorant of C#, but from my perspective as a J coder I was more comfortable with what LEG(G) had written.
Out of this I’ve gained the impression that TDD is a technique intended to favor a particular style of coding. In the past I’ve heard TDD promoted as a technique that makes it easier to write “better” code, but without containing claims as to what counts as “better.” Now I’m thinking TDD (as we know it) is being advocated by people who have particular preferences, and that what TDD helps most is implementing according to these preferences.
If this is so, what does it imply for languages that don’t fit the preferred style? If this is not the case, how would we criticize this notion to identify the error?