(This is part of a post I wrote to Agile Finland mailing list. You can read the entire thread there.)
The three ways to design software
There are three approaches to software design: top-down, bottom-up and outside-in. (People mean different things when they say top-down, so don’t be surprised if my definition doesn’t match yours. What I am describing here should be taken as a clarification of the rest of the post, not as a normative definition. The same goes to bottom-up and outside-in.)
Bottom-up means you start with the “smallest things”. For example, you know that you are going to need a custom communication protocol for your distributed application, so you start by writing the code for that. Then you write – let’s say – database code and then UI code and finally something to glue them all together. The overall design becomes apparent only when you have all the modules ready.
Top-down starts with the overall design. You find modules and interfaces between them, then go on to design class hierarchies and interfaces inside individual classes. You go into smaller and smaller detail until you reach the code level. At that point your design is ready and you start the actual implementation. This is the classical sequential approach to software process.
Outside-in is an iterative method. You start with one user story, and write code for that. Then you go on to the next user story, and so on. You start implementing from the code that is closest to the user and proceed toward more and more implementation detail. In a web application (using TDD) this order could be: acceptance test, route test, route code, view test, view code, controller test, controller code, model test, model code. You only start writing code for a lower level when the upper level requires that.
The understanding – or fashion – today is that TDD is best done outside-in. It may be a little more difficult technically, since you need to isolate layers somehow, so that you don’t have too many failing tests in your green-red cycle. Typically mock objects are used for this. Of course isolating unit tests is a good idea regardless of your approach, so you may want to use mock objects anyway.
Now the important question of course is, how do you make sure that the overall design works with outside-in process. In my opinion it doesn’t hurt to do a little top-down design before you start the outside-in cycles. However, be careful not to over-do it. Using a popular framework helps. The top-down design phase can be very short when you have something well-known and well-tested as the foundation of your architecture.
After the rough design is ready, the first few user story iterations should strive to validate that architecture (especially if you have rolled out your own). Architectural redesign is cheap early in the project but very costly later on. Using outside-in is an improvement over a sequential approach in this sense. With a waterfall process you don’t get feedback until much later, and if the architecture doesn’t work, it will be usually too late to fix it.
Responding to requirement changes
Another important consideration is how you deal with requirement changes, as there will be some in every project. It is interesting to note the differences between the three approaches in this regard.
With bottom-up, we have small isolated libraries. The hope is that we can reuse them easily when the need arises. Also, a well used bottom-up design builds up the abstraction level in every layer. The top levels will be easy to understand and easy to change. This is the Paul Graham argument for bottom-up design.
Top-down takes care of extendability by carefully desigining extension points in the architectural design. Elaborate class hierarchies and design patterns are used to make reuse and modifiability as easy as possible.
Outside-in doesn’t concentrate on modifiability per se. The belief is that the best way to provide malleability is to write the simplest – or minimal – code possible. When there is nothing that is not needed, the purpose and the functionality of the code is easy to understand and easy to modify.
Experience has shown that the top-down approach to modifiability doesn’t work. Designing for extendability makes the code unnecessarily complicated, slowing the implementation process. What’s worse is that the extension points usually end up in the wrong places. The requirement changes often come as surprise, in a place you wouldn’t expect. The original design didn’t allow for extendability after all, and an expensive redesign is needed.
Compared with bottom-up, the advantage of outside-in again is the minimality of the code. Writing a module with bottom-up approach it is hard to guess what is actually needed by the upper layers. The interfaces will have unnecessary classes and methods. This takes time, and makes the code harder to use.
Of course my discussion here is both simplified and polarized for the sake of the argument. In real life you will not make a clear-cut choice between the three approaches and stick with it, but you will use whatever approach you believe works best in each situation.