Taking a cue from Julia Evans, linking this here to remind myself to re-read it occasionally:
Sunday, November 9, 2014
There are, I think, two big dangers in designing software (which probably apply to other types of creative activity, as well):
- No up-front design.
- Requiring complete design to begin implementation.
I call these the two "big" dangers because there are strongly seductive aspects to each, and because each approach has been followed in ways that have driven projects into the ground. I think these approaches are followed because many project planners like to frame the plan in terms of "how much design is necessary before implementation starts?". When you ask the question this way, "no up-front design" and "no implementation without a coherent design" are the simplest possible answers, and both of these answers is reasonable in a way:
- When avoiding up-front design, you can get straight to the implementation, allowing you to show results early, ultimately allowing you to get earlier feedback from your target audience about whether you're building the correct product.
- When avoiding implementation before the design is fleshed out, you can be more confident that parts of the implementation will fit together as intended, that fewer parts of the system will require significant rework when requirements change late in the process, and it is much easier to involve more people in the implementation, since you should be able to rely more on the formal design documentation to allow work to coordinate.
(I could also make a list of the cons, but ultimately the cons of each approach are visible in the pros of the other. For example, by avoiding up-front design, it becomes much harder to scale the implementation effort to a larger group than, say, 10 or 15 people: The cost of producing formal specifications is high, but (relatively) fixed, while the cost of informal communications starts low, but increases with the square of the number of individuals in the group.)
As I write today, the dominant public thinking in the software developer community is broadly aligned against Big Design Up Front, and towards incremental, or emergent design. I generally share this bias: I consider software design to be the art of learning what the constructed system should look like, in enough detail that the system becomes computer operational (Design Is Learning), and I think learning is better facilitated when we have earlier opportunity for feedback from our design decisions. Further, if we make mistakes in our early design, it's much less expensive to fix those mistakes directly after making them than it is after they become ingrained in our resulting system. It's extremely important to get feedback about the suitability of our design decisions quickly after making them.
But the importance of early feedback does not reduce the importance of early big-picture thinking about the design. I work mostly in real-time embedded systems programming, and in this domain, you cannot ignore the structure of the execution path, or even of the data accessed, between event stimulus and time-critical response. Several operations would have been easier to implement had I ignored these real-time concerns, and the problems that would have resulted would have been invisible in early development (when the system wasn't as stressed, and real-time constraints were looser). On the other hand, we would not have been able to make that system work in that form: large amounts of code would have likely needed rewrite to meet higher load and tighter timing constraints as our system got closer to market. The extra effort put into being able to control the timing of specific parts of our execution path was critical to our system's ability to adapt to tightening real-time requirements.
Which all serves to introduce the core four words of this little essay: "Think Big, Act Small." In other words, consider the whole context of the design you are working on ("think big"), while making sure to frequently test that you are moving towards your goal ("act small"). So, if I'm working on a part of the system, I don't think only of that part, but also of all the other parts with which it will interact, of its current use and of its possible future uses, and of the constraints that this part of the system must operate under. (That is, I try to understand the part's whole context as I design and build it.) On the other hand, if I'm trying to design some large-scale feature, I think of how it breaks down into pieces, I try to think about which of these pieces I'm likely to need the soonest, what sorts of requirements changes are likely to occur, what parts of the break-down those changes are likely to affect the most, and, of the big design, how much work do we actually need to do to meet our immediate needs. (That is, I try to break down the big design into the smallest steps I can that will a) demonstrate progress towards the immediate goal, and b) be consistent with likely future changes.) By the time I actually begin to write the software code, I have probably thought about an order of magnitude larger portion of the system than what I will write to complete my immediate task.
This is hard work, and it can feel like waste to spend time designing software that, often, will never be written. Patience and nerves get worn out, trying to hold a large part of the system in my head at once before the design of a feature or subsystem finally gels and implementation can start. On the other hand, I've found the designs I've created in this way have tended to be much more stable, and maintenance on individual modules tends not to disturb other parts of the system (unless the maintenance task naturally touches that other part of the system as well). In other words, I feel these designs are very well factored. It takes a lot of effort to get there, but echoing an idea made famous by Eisenhower ("plans are worthless, but planning is everything"), in the end I would rather spend up-front time thinking about how to write code that will never need to be written, than spend time at the back-end thinking about how to re-write subsystems that will never meet their design objectives.
Think big: what is the whole context of the effort you are thinking of undertaking? Act small: how can you find out if the path you are on is correct, as early as possible? Know your objectives early, test your decisions early, and adapt to difficulties early, to achieve your goals.
(PS: for more on this theme, see here.)