- Reduced Time in Up-Front Design
- Refactoring Versus Up-Front Design
- Reduced Time Producing Non-Executable Documentation
- Reduced Time in Reading/Updating Materials
- Less Time Wasted Reading Inaccurate Materials
- Reduced Debugging Time
- Reduced Number of Defects
- Reduced "Mulling" Time
- Reduced Amount of Code and Increased Reuse
- In Conclusion
Refactoring Versus Up-Front Design
For a system to be able to support the addition of new requirements every week or two as part of an agile development process, it must have an optimally straightforward design. The cardinal rule of TDD is that after every small cycle of introducing test and production code, you must refactor the system so that it has the best possible design for the existing feature set. In fact, this rule is so important that I view refactoring as perhaps the most critical piece of making agile software development sustainable.
Of course, refactoring isn’t free, but I can include it in my development cycle. For every few minutes of building code, I spend a few minutes cleaning it up. Done in this fashion, refactoring is invisible, and just part of building quality code. In fact, it’s essential to refactor in this fashion—otherwise, you build up a lot of "refactoring debt" that you must pay off later.
You might think that it’s preferable to do most of the design up front: "Yes, maybe it’s impossible to create a perfect design. But why don’t we just come up with a design that’s as normalized as we think we’ll need, one that can handle anything that comes along?"
Well, there are many reasons why not:
- It’s easier to take a simple, straightforward design and make it more abstract than to take an incorrect generic design and rework all of the classes it involves. I’ve done both, many times. Lots of poorly designed abstractions create more sticky dependencies than fewer, well-designed concrete classes.
- Coming up with a "perfect" design takes a long
time. Up-front design time always includes a considerable amount of
waste: It takes longer to determine whether we have the proper design, and,
inevitably, parts of that design are not correct. We attend lots of
meetings involving lots of people and taking lots of hours. We expend
considerable effort in putting the design onto paper in an unambiguous form.
Yes, refactoring sometimes involves reworking things that you might have been
able to think of in advance, but you make that tradeoff against this costly
up-front design time. I’ll talk more about the cost of documentation in
the next section.
Suppose you get your generic design perfect for a product’s initial release, which takes one year to build and deploy. Almost all products survive long past their initial deployment. Inevitably, you’ll need to make changes to your perfect design in order to accommodate new features. These new features will involve things you never thought of when coming up with your original, perfect design.
It seems easier to consider that new features are always coming. As long as we keep the system clean, we can readily adapt the design to these new changes in an incremental fashion. Sometimes it will cost more to adapt the design, but the value in this adaptability is huge.
- A more normalized design makes the system more complex and difficult to understand. A design that accommodates all potential forthcoming requirements must by definition be highly abstract. Programmers have a difficult time enough time understanding abstractions in systems. An overdesigned system is difficult to work with. Unnecessary complexity increases all future maintenance and development costs.
- Many of the promised features never come. Efforts spent in
over-generalizing the design often go to waste. I have seen this waste happen
often. One client was certain that they had to have internationalization support
in the upcoming release. Then the marketing emphasis shifted dramatically to
domestic sales. No more need for internationalization!
If you follow TDD, you’re eliminating duplication wherever it exists. Consider text embedded in production code. If you write unit tests, that text must by definition exist in two places: It exists in the production code as well as in the test. If you eliminate the duplication between the two, you travel a significant way toward supporting that internationalization requirement.