On December 22, 2010 Mark Seemann published a blog post The TDD Apostate. Since then several friends and associates have sent me links to his post as if to say, “See, TDD is not all it is cracked up to be.” It is an excellent post and I agree with much of what the author says as well as the comments by his readers but there are a few points he makes that seem misleading and perhaps underrate the true value of TDD for those who do not practice it.
One of the challenges about discussing TDD and design is that different people have different views of what TDD is. For the purposes of this post (and my feelings about TDD in general) when I refer to TDD I mean test first development that strives for 100% code coverage but no more (for more on the misuse of TDD please see my other posts: Triangulation and That’s Not TDD as well as my many posts on effective ways to use TDD).
I believe the author’s point in his post was that you can’t blindly follow TDD and necessarily end up with a good design. That is true because you can’t blindly follow any approach and expect to come up with a good design, if you could then computers would write software and programmers would be out of a job.
However, with the dimmest awareness of a few key things, I believe that test first development can help us emerge good designs and often shows us good design alternatives if we pay attention.
The author used Bob Martin’s SOLID principles as his criteria for good design. Out of the 5 SOLID principles (Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle and Dependency Inversion Principle) he says TDD only helps with the Interface Segregation Principle and a little with the Open-Closed Principle so he scores TDD with 1.5 out of 5 for driving us towards the SOLID principles.
Here’s how I score it:
SRP: “God objects” are very hard to test because they have so many responsibilities. I believe TDD supports making classes and methods have a single responsibility so they are easier to test. For example, if I have a class with 6 responsibilities I’d need 32 tests to ensure those responsibilities don’t interact (the formula is 2^n). If I have 6 classes that encapsulate each of the 6 responsibilities then I only need 6 tests (plus integration) to ensure they work. If I want to make it home for dinner instead of writing tests all night then I’ll follow TDD as it drives me towards the Single Responsibility Principle and make my classes and method have only one responsibility so testing is easier. I count one for TDD.
OCP: The author gives the Open-Closed Principle half a point; I give it one full point because one of the easiest ways to make an issue open for extension is to put it behind an abstraction and we know this is good design (when it is not over-design). When we encapsulate issues behind abstractions we build better, more robust code. Furthermore, OCP helps us identify responsibilities so we can more easily mock them out to test and helps us push our business rules to factories instead of spreading them out across our system. Again, TDD shows me that I must mock out everything not under control of the test which often makes these issues open for extension later. This makes 2 points for TDD.
LIS: I agree with the author; no point here and I think this is an important distinction. TDD does not drive us to make superclasses substitutable for their subclasses. Of course, my car does not drive me to the store, I drive it. I drive TDD to use the Liskov Substitution Principle but I do not think that blindly following the mechanism of TDD gets me to follow the Liskov Substation Principle so I agree with the author on this point. The score is still 2 points for TDD.
ISP: Disagree, for the same reason as SRP. Multiple, unrelated reasons for calling the same API is a cohesion issue and it is also harder to mock. Because of possible interactions of multiple orthogonal callers we would need more tests than if we segregated the interfaces. Following Interface Segregation Principle makes code easier to test. My score so far is 3 points for TDD.
DIP: I agree with the author, TDD does drive us towards the Dependency Inversion Principle and helps us design interfaces based on the client’s needs. As my friend and colleague Scott Bain likes to say, “Think of the test as your first client.” When we write the test first we are focusing on building a stable signature for our methods up front and this is good design. The score is 4 points for TDD.
Total: My score is 4 points out of 5 points for TDD. I think that is pretty good. I think TDD does help us build better code based on the SOLID principles but the SOLID principles are just one way to get better code and just because code follows the SOLID principles does not mean that it is well designed. We must also employ other principles, practices and techniques.
What TDD does not help us with is finding the right abstractions; that requires other techniques. I agree with the author on this point but we shouldn’t infer that TDD is worthless. We can’t build a house with only a hammer but that doesn’t mean hammers are useless when building a house.
I read an article a few years ago from one of the developers at Object Mentor who was showing how you can use TDD to emerge the design of a parser. I can’t locate the article now but I recall it was about 50 pages long and it showed how, based on need, to emerge the design of a full parser just in time and without a lot of reworking. It was very interesting. Unfortunately, the design the author emerged to was a giant switch statement but if this author knew patterns I’m sure he would have been refactoring to a Chain of Responsibility and he would have ended up with a great design.
TDD is only a tool–a vehicle–but we still must drive it with the intention of getting somewhere. The SOLID principles give us some guidance with design but the problem with principles is they tell us what to strive for but not how to get it. The SOLID principles specifically point to some aspects of good design but only conforming to these principles do not guarantee a good design. I believe if I had to limit my design guidance to the bare minimum I would focus on code qualities rather than the SOLID principles. Fortunately, I can use both and together with a good understanding of patterns and some basic practices I believe TDD can help us emerge good designs as needed much of the time.
I have a list of 18 principles, practices, techniques and qualities that I teach in my Software Development Essentials classes. You cannot blindly follow them to get a good design no more than you can blindly wield carpentry tools and expect to build a beautiful home. However, with some intelligence and awareness you can use TDD and the other 17 things along with about a dozen design patterns and have a powerful set of tools to help design in most situations.
What I believe the mechanism of TDD (test first TDD) helps us with is sequence. A list of ingredients does not make a recipe, we also need the instructions to show how to combine them and this is often what is missing when developers try to emerge designs. TDD can help show us the “when” of emergent design based on needs and unfolding requirements and the refactor step in TDD tells us when we should focus on design in the small but we must use other techniques to show us the “how” of good design.
Building software is one of the most complex activities we can engage in. Good software design take experience, more experience than sitting through a 3-day class. I’ve been at it for over a quarter of a century so I’ve seen how various design approaches play out. I know the common mistakes that developers tend to make and I carefully selected those 18 things to address them. At the core of everything are 6 code qualities that I believe all good principles and practices proceed from.
So what are these mysterious code qualities? Stick around. In the coming weeks, through a series of blog posts, I will introduce to you 6 code qualities that you can use to evaluate a design and drive towards better designs (with your eyes open, of course).