“Bottom-up design is becoming more important as software grows in complexity. Programs today may have to meet specifications which are extremely complex, or even open-ended. Under such circumstances, the traditional top-down method sometimes breaks down. In its place there has evolved a style of programming quite different from what is currently taught in most computer science courses: a bottom-up style in which a program is written as a series of layers, each one acting as a sort of programming language for the one above.” (On Lisp, p v.)
“This is the approach of stratified design, the notion that a complex system should be structured as a sequence of levels that are described using a sequence of languages. Each level is constructed by combining parts that are regarded as primitive at that level, and the parts constructed at each level are used as primitives at the next level. The language used at each level of a stratified design has primitives, means of combination, and means of abstraction appropriate to that level of detail.” (SICP Section 2.2)
I spent a few hours fiddling with some C# 3.5 code recently– it was pretty jarring considering how much time I’d spent with Lisp this past while. The compile/execute/manual-test loop seems incredibly slow to me. Painstakingly slow, even. As I worked, it crossed my mind that the whole Test-Driven-Development meme was probably spawned by the fact that impoverished developers across the world were attempting to code applications without the benefit of a REPL. Yeah, the main benefit of Unit Tests is that you get control of your codebase so that you’re not afraid to refactor and improve it. But with TDD you can end up coding a lot of unnecessary tests that are just there because it’s so time consuming to interact with your code. If size is the enemy, then any unnecessary tests are going to take away valuable momentum from your project.
But the TDD advocate will surely argue back that the “real” benefit of their technique is to help you design your interfaces and object models correctly as you go– which is well worth a little extra code that mainly helps document the system. I would argue that such an approach may not be deep enough for the long term. Yes, things will be pretty in your IDE when you hit the “.” key and the IntelliSense(tm) window pops up. But the granularity of your object model may not be fine enough to allow for a really expressive and adaptable design.
An alternative (or complement) to TDD would be Language Driven Development. Don’t start with tests or object models. Start with a language. Make up an imaginary language that will handle everything you want your program to do. The main benefit of this is that you can think about the entire application at one time and see commonalities between disparate parts of the architecture. Write up lots of examples of how you would implement common tasks in the language and make sure you have the main bases covered, and then try to imagine the machinery that would be required to execute the pseudo code. Now you will be much more likely to come up with powerful abstractions because the methodology is forcing you to think in terms of at least two complementary directions. You’re working top-down when you spec out your imaginary language… and you’re working bottom-up when your define the underlying machinery. Good programming requires a person that can move deftly between both levels– and TDD alone can be dangerously myopic.
If you can identify ways of stratifying your design, you’ll need fewer unit tests in the long term. With proper abstraction barriers between the layers of your design, you won’t be as dependent on exhaustive unit tests when you want to refactor your code. Instead of a sprawling pyramid with custom tests for each brick, you could instead code tests for a smaller number of generic components at each level.