In my experience good TDD designs tend to have evolved (and hopefully continue to evolve) in a sort of punctuated equilibrium. There are long periods of small incremental changes to a code base, punctuated by infrequent bursts of large design-level refactorings. While the TDD mantra is “Red, Green, Refactor”, I think a more accurate version would be “Red, Green, Red, Green, Red, Green, Refactor”. Less catchy, I know.
Letting tests drive your design is great, but if you’re not careful you will end up with a design which is the result of a simple accreation of functionality. This of course is the reason for the “Refactor” part of RGR. However, in my experience the refactoring part does not occur in a steady flow. Rather there will be periods of time where only small implementation-level refactorings take place during the RGR cycle as new functionality is implemented. Then at some point something will give. Someone may have a design breakthrough, realizing a more expressive/elegant/clean way to express some functionality. Alternatively some niggling annoyance in a subsystem’s design will pass the team’s tolerance threshold such that they decide they need to clean up the design to remove the annoyance. Both events will lead to a brief flurry of larger design-level refactorings.
A somewhat minor example of the ‘niggling annoyance’ case would be when someone is adding a 4th optional parameter to a method call and finally decides to bite the bullet and pull all the optional parameters out into a single options hash. An example of the ‘design breakthrough’ case might be the third time someone adds a subclass in order to specialize some aspect of a base classes functionality. They might realize that really what’s going on here is that each subclass represents a Strategy, which can be plugged into the base class. Now both of these examples are rather small-scale, but hopefully they illustrate the concept. It’s hard to come up with examples of larger refactorings which are easy to succinctly describe without a bunch of background information.
A key observation here is that principles like YAGNI and ‘do the simplest thing that will work’ will not work well in isolation. Rather they must live alongside principles like DRY, seperation of concerns, do one thing and do it well, etc. If your sole focus is on doing the minimum amount to get your tests passing you will not end up with a supple code base which is amenable to change. That requires dilligent attention on your part.
Does this mean that TDD leads to bad design? No. TDD does not lead to good design on its own, but it does set the stage for good design to emerge. A team with the safety net of a strong test suite has the courage to follow through and realize these design-level refactorings. A team without that safety net may know exactly what needs to be done but will lack the courage to do so, because they know that any changes will lead to unknown breakages. Thus, the team will be reluctant to undertake these design improvements, as much as they would like to do so.
Finally, this idea of punctuated design equilibrium may sound familiar to folks who’ve read Eric Evan’s Domain Driven Design. He talks about how good teams who are really working to understand a domain will occasionally find themselves up against a breakthrough which requires them to stop forward momentum on features in order to realize a more elegant expression of their core domain. That small section of his really excellent book was certainly in my mind as I started writing this post.