- Evolutionary Design Illustrated is a highly visual look at how incremental design and architecture works in practice.
- Let's Play: Test-Driven Development is an extensive screencast series demonstrating incremental design on a real project.
The following text is excerpted from The Art of Agile Development by James Shore and Shane Warden, published by O'Reilly. Copyright © 2008 the authors. All rights reserved.
Incremental Design and Architecture
We deliver stories every week without compromising design quality.
XP makes challenging demands of its programmers: every week, programmers should finish four to ten customer-valued stories. Every week, customers may revise the current plan and introduce entirely new stories—with no advance notice. This regimen starts with the first week of the project.
In other words, as a programmer you must be able to produce customer value, from scratch, in a single week. No advance preparation is possible. You can't set aside several weeks for building a domain model or persistence framework: your customers need you to deliver completed stories.
Fortunately, XP provides a solution for this dilemma: incremental design (also called evolutionary design) allows you to build technical infrastructure (such as domain models and persistence frameworks) incrementally, in small pieces, as you deliver stories.
How It Works
Incremental design applies the concepts introduced in test-driven development to all levels of design. Like test-driven development, developers work in small steps, proving each before moving to the next. This takes place in three parts: start by creating the simplest design that could possibly work, incrementally add to it as the needs of the software evolve, and continuously improve the design by reflecting on its strengths and weaknesses.
- Simple Design
To be specific, when you first create a design element—whether it's a new method, a new class, or a new architecture—be completely specific. Create a simple design that solves only the problem you face at the moment, no matter how easy it may seem to solve more general problems.
This is difficult! Experienced programmers think in abstractions. In fact, the ability to think in abstractions is often a sign of a good programmer. Coding for one specific scenario will seem strange, even unprofessional.
Waiting to create abstractions will enable you to create designs that are simple and powerful.
Do it anyway. The abstractions will come. Waiting to make them will enable you to create designs that are simpler and more powerful.
The second time you work with a design element, modify the design to make it more general—but only general enough to solve the two problems it needs to solve. Next, review the design and make improvements. Simplify and clarify the code.
The third time you work with a design element, generalize it further—but again, just enough to solve the three problems at hand. A small tweak to the design is usually enough. It will be pretty general at this point. Again, review the design, simplify, and clarify.
Continue this pattern. By the fourth or fifth time you work with a design element—be it a method, a class, or something bigger—you'll typically find that its abstraction is perfect for your needs. Best of all, because you allowed practical needs to drive your design, it will be simple yet powerful.
You can see this process in action in test-driven development. TDD is an example of incremental design at the level of methods and individual classes. Incremental design goes further than TDD, however, scaling to classes, packages, and even application architecture.
I have to admit I was very skeptical of incremental design when I first heard about it. I felt that up-front design was the only responsible approach. The first time my team tried incremental design, we mixed up-front design with incremental design. We designed the architecture up-front, feeling it was too important to leave until later. Over time, however, experience showed us that many of those initial design decisions had serious flaws. We used incremental design not only to fix the flaws, but to produce far better alternatives. When I left the project eighteen months later, it had the best design of any code I've ever seen.
That project taught me to trust incremental design. It's different from traditional design approaches, but it's also strikingly effective—and more forgiving of mistakes. Software projects usually succumb to bit rot and get more difficult to modify over time. With incremental design, the reverse tends to be true: software actually gets easier to modify over time. Incremental design is so effective, it's now my preferred design approach for any project, XP or otherwise.
Incremental design initially creates every design element—method, class, namespace, or even architecture—to solve a specific problem. Additional customer requests guide the incremental evolution of the design. This requires continuous attention to the design, albeit at different time-scales. Methods evolve in minutes; architectures evolve over months.
No matter what level of design you're looking at, the design tends to improve in bursts. Typically, you'll implement code into the existing design for several cycles, making minor changes as you go. Then something will give you an idea for a new design approach, requiring a series of refactorings to support it. [Evans] calls this a breakthrough (see Figure). Breakthroughs happen at all levels of the design, from methods to architectures.
Breakthroughs are the result of important insights and lead to substantial improvements to the design. (If they don't, they're not worth implementing.) You can see a small, method-scale breakthrough at the end of "A TDD Example" earlier in this chapter.
Incrementally Designing Methods
You've seen this level of incremental design before: it's test-driven development. While the driver implements, the navigator thinks about the design. She looks for overly complex code and missing elements, which she writes on her notecard. She thinks about which features the code should support next, what design changes might be necessary, and which tests may guide the code in the proper direction. During the refactoring step of TDD, both members of the pair look at the code, discuss opportunities for improvements, and review the navigator's notes.
The roles of driver and navigator aren't as cut-and-dried as I imply. It's okay for drivers to think about design and for navigators to make implementation suggestions.
Method refactorings happen every few minutes. Breakthroughs may happen several times per hour and could take ten minutes or more to complete.
Incrementally Designing Classes
When TDD is done well, the design of individual classes and methods is beautiful: they're simple, elegant, and easy to use. This isn't enough. Without attention to the interaction between classes, the overall system design will be muddy and confusing.
During TDD, the navigator should also consider the wider scope. Ask yourself these questions: are there similarities between the code you're implementing and other code in the system? Are class responsibilities clearly defined and concepts clearly represented? How well does this class interact with other classes?
When you see a problem, jot it down on your card. During one of the refactoring steps of TDD—usually, when you're not in the middle of something else—bring up the issue, discuss solutions with your partner, and refactor. If you feel that your design change will significantly affect other members of the team, take a quick break to discuss it around a whiteboard.
Don't let design discussions turn into long, drawn-out disagreements. Follow the ten-minute rule: if you disagree on a design direction for ten minutes, try one and see how it works in practice. If you have a particularly strong disagreement, split up and try both as spike solutions. Nothing clarifies a design issue like working code.
Class-level refactorings happen several times per day. Depending on your design, breakthroughs may happen a few times per week and can take several hours to complete. (Nonetheless, remember to proceed in small, test-verified steps.) Use your iteration slack to complete breakthrough refactorings. In some cases, you'll won't have time to finish all of the refactorings you identify. That's okay; as long as the design is better at the end of the week than it was at the beginning, you're doing enough.
TODOcomments or story/task cards for postponed refactorings. If the problem is common enough for you or others to notice it again, it will get fixed eventually. If not, then it probably isn't worth fixing. There are always more opportunities to refactor than time to do it all;
TODOs or refactoring cards add undue stress to the team without adding much value.
Incrementally Designing Architecture
Large programs use overarching organizational structures called architecture. For example, many programs segregate user interface classes, business logic classes, and persistence classes into their own namespaces; this is a classic three-layer architecture. Other designs have the application pass the flow of control from one machine to the next in an n-tier architecture.
These architectures are implemented through the use of recurring patterns. These aren't design patterns in the formal Gang of Four1 sense. Instead, they're standard conventions specific to your codebase. For example, in a three-layer architecture, every business logic class will probably be part of a "business logic" namespace, may inherit from a generic "business object" base class, and probably interfaces with its persistence layer counterpart in a standard way.
1The "Gang of Four" is a common nickname for the authors of Design Patterns, a book that introduced design patterns to the mainstream.
These recurring patterns embody your application architecture. Although they lead to consistent code, they're also a form of duplication, which makes changes to your architecture more difficult.
Fortunately, you can design architectures incrementally. As with other types of continuous design, use TDD and pair programming as your primary vehicle. While your software grows, be conservative in introducing new architectural patterns: introduce just what you need to for the amount of code you have and the features you support at the moment. Before introducing a new pattern, ask yourself if the duplication is really necessary. Perhaps there's some language feature you can use that will reduce your need to rely on the pattern.
In my experience, breakthroughs in architecture happen every few months. (This estimate will vary widely depending on your team members and code quality.) Refactoring to support the breakthrough can take several weeks or longer because of the amount of duplication involved. Although changes to your architecture may be tedious, they usually aren't difficult once you've identified the new architectural pattern. Start by trying out the new pattern in just one part of your design. Let it sit for a while—a week or two—to make sure the change works well in practice. Once you're sure that it does, bring the rest of the system into compliance with the new structure. Refactor each class that you touch as you perform your everyday work and use some of your slack in each iteration to fix other classes.
Balance technical excellence with delivering value.
Keep delivering stories while you refactor. Although you could take a break from new development to refactor, that would disenfranchise your customers. Balance technical excellence with delivering value. Neither can take precedence over the other. This may lead to inconsistencies within the code during the changeover, but fortunately, that's mostly an aesthetic problem—more annoying than problematic.
Introducing architectural patterns incrementally helps reduce the need for multi-iteration refactorings. It's easier to expand an architecture than it is to simplify one that's too ambitious.
Architecture may seem too essential not to design up front. Some problems do seem too expensive to solve incrementally, but I've found that nearly everything is easy to change if you eliminate duplication and embrace simplicity. Common thought is that distributed processing, persistence, internationalization, security, and transaction structure are so complex that you must consider them from the start of your project. I disagree; I've dealt with all of them incrementally [Shore 2004a].
Two issues that remain difficult to change are choice of programming language and platform. I wouldn't want to make those decisions incrementally!
- Simple Design
Of course, no design is perfect. Even with simple design, some of your code will contain duplication, and some will be too complex. There's always more refactoring to do than time to do it. That's where risk-driven architecture comes in.
Although I've emphasized designing for the present, it's okay to think about future problems. Just don't implement any solutions to stories that you haven't yet scheduled.
What do you do when you see a hard problem coming? For example, what if you know that internationalizing your code is expensive and only going to get more expensive? Your power lies in your ability to chooose which refactorings to work on. Although it would be inappropriate to implement features your customers haven't asked for, you can direct your refactoring efforts towards reducing risk. Anything that improves the current design is okay—so choose improvements that also reduce future risk.
To apply risk-driven architecture, consider what it is about your design that concerns you and eliminate duplication around those concepts. For example, if your internationalization concern is that you always format numbers, dates, and other variables in the local style, look for ways to reduce duplication in your variable formatting. One way to do so would be to make sure every concept has its own class (as described in Simple Design earlier in this chapter), then condense all formatting around each concept into a single method within each class, as shown in Figure. If there's still a lot of duplication, the Strategy pattern would allow you to condense the formatting code even further.
Limit your efforts to improving your existing design. Don't actually implement support for localized formats until your customers ask for them. Once you've eliminated duplication around a concept—for example, once there's only one method in your entire system that renders numbers as strings—changing its implementation will be just as easy later as it is now.
A team I worked with replaced an entire database connection pooling library in half a pair-day. Although we didn't anticipate this need, it was still easy because we had previously eliminated all duplication around database connection management. There was just one method in the entire system that created, opened, and closed connections, which made writing our own connection pool manager almost trivially easy.2
2We did have to make it thread-safe, so it wasn't entirely trivial.
Using Stories to Reduce Risk
Another great way to reduce technical risk is to ask your customers to schedule stories that will allow you to work on the risky area. For example, to address the number localization risk, you could create a story such as "Localize application for Spain" (or any European country). This story expresses customer value, yet addresses an internationalization risk.
Your customers have final say over story priorities, however, and their sense of risk and value may not match yours. Don't feel too bad if this happens; you can still use refactorings to reduce your risk.
It's Not Just Coding
Although incremental design focuses heavily on test-driven development and refactoring as an enabler, it isn't about coding. When you use TDD, incremental design, and pair programming well, every pairing session involves a lot of conversation about design. In fact, that's what all the (relevant) conversations are about. As Ron Jeffries likes to say, design is so important in XP that we do it all the time. Some of the design discussions are very detailed and nitpicky, such as "What should we name this method?" Others are much higher level, such as "These two classes share some responsibilities. We should split them apart and make a third class."
Have design discussions away from the keyboard as often as you think is necessary, and use whatever modelling techniques you find helpful. Try to keep them informal and collaborative; sketches on a whiteboard work well. Some people like to use CRC (Class, Responsibility, Collaborator) cards.
Some of your discussions will be predictive: you'll discuss how you can change your design to support some feature that you haven't yet added to the code. Others will be reflective: you'll discuss how to change your design to better support existing features.
Beware of getting trapped in analysis paralysis and spending too much time trying to figure out a design. If a design direction isn't clear after ten minutes or so, you probably need more information. Continue using TDD and making those refactorings that are obvious, and a solution will eventually become clear.
Reflective design (discussed in more detail in Refactoring earlier in this chapter) is always helpful in XP. I like to sketch UML diagrams on a whiteboard to illustrate problems in the current design and possible solutions. When my teams discover a breakthrough refactoring at the class or architecture level, we gather around a whiteboard to discuss it and sketch our options.
Predictive design is less helpful in XP, but it's still a useful tool. As you're adding a new feature, use it to help you decide which tests to write next in TDD. At a larger scale, use predictive design to consider risks and perform risk-driven architecture.
The trick to using predictive design in XP is keeping your design simple and focusing only on the features it currently supports. Although it's okay to predict how your design will change when you add new features, you shouldn't actually implement those changes until you're working on the stories in question. When you do, you should keep your mind open to other ways of implementing those features. Sometimes the act of coding with test-driven development will reveal possibilities you hadn't considered.
Given these caveats, I find that I use predictive design less and less as I become more experienced with incremental design. That's not because it's "against the rules"—I'm perfectly happy breaking rules—but because working incrementally and reflectively has genuinely yielded better results for me.
Try it yourself, and find the balance between predictive and reflective design that works best for you.
Isn't incremental design more expensive than up-front design?
Just the opposite, actually, in my experience. There are two reasons for this. First, because incremental design implements just enough code to support the current requirements, you start delivering features much more quickly with incremental design. Second, when a predicted requirement changes, you haven't coded any parts of the design to support it, so you haven't wasted any effort.
Even if requirements never changed, incremental design would still be more effective, as it leads to design breakthroughs on a regular basis. Each breakthrough allows you to see new possibilities and eventually leads to another breakthrough—sort of like walking through a hilly forest in which the top of each hill reveals a new, higher hill that you couldn't see before. This ongoing series of breakthroughs substantially improves your design.
What if we get the design absolutely wrong and have to backtrack to add a new feature?
Sometimes a breakthrough will lead you to see a completely new way of approaching your design. In this case, refactoring may seem like backtracking. This happens to everyone and is not a bad thing. The nature of breakthroughs—especially at the class and architectural level—is that you usually don't see them until after you've lived with the current design for a while.
Our organization (or customer) requires comprehensive design documentation. How can we satisfy this requirement?
Ask them to schedule it with a story, then estimate and deliver it as you would any other story. Remind them that the design will change over time. The most effective option is to schedule documentation stories for the last iteration.
If your organization requires up-front design documentation, the only way to provide it is to engage in up-front design. Try to keep your design efforts small and simple. If you can, use incremental design once you actually start coding.
When you use incremental design well, every iteration advances the software's features and design in equal measure. You have no need to skip coding for an iteration for refactoring or design. Every week, the quality of the software is better than it was the week before. As time goes on, the software becomes increasingly easy to maintain and extend.
Incremental design requires simple design and constant improvement. Don't try to use incremental design without a commitment to continuous daily improvement (in XP terms, merciless refactoring.) This requires self-discipline and a strong desire for high-quality code from at least one team member. Because nobody can do that all the time, pair programming, collective code ownership, energized work, and slack are important support mechanisms.
Test-driven development is also important for incremental design. Its explicit refactoring step, repeated every few minutes, gives pairs continual opportunities to stop and make design improvements. Pair programming helps in this area, too, by making sure that half of the team's programmers—as navigators—always have an opportunity to consider design improvements.
Be sure your team sits together and communicates well if you're using incremental design. Without constant communication about class and architectural refactorings, your design will fragment and diverge. Agree on coding standards so that everyone follows the same patterns.
Anything that makes continuous improvement difficult will make incremental design difficult. Published interfaces are an example; because they are difficult to change after publication, incremental design may not be appropriate for published interfaces. (You can still use incremental design for the implementation of those interfaces.) Similarly, any language or platform that makes refactoring difficult will also inhibit your use of incremental design.
Finally, some organizations place organizational rather than technical impediments on refactoring, as with organizations that require up-front design documentation or have rigidly controlled database schemas. Incremental design may not be appropriate in these situations.
If you are uncomfortable with XP's approach to incremental design, you can hedge your bets by combining it with up-front design. Start with an up-front design stage and then commit completely to XP-style incremental design. Although it will delay the start of your first iteration (and may require some up-front requirements work, too), this approach has the advantage of providing a safety net without incurring too much risk.
If you're feeling bold, though, use XP's iterative design directly, without the safety net. Incremental design is powerful, effective, and inexpensive. The added effort of an up-front design stage isn't necessary.
There are other alternatives to XP's approach to incremental design, but I don't think they would work well with XP. One option is to use another type of incremental design, one more like up-front design, that does some up-front design at the beginning of every iteration, rather than relying on simple design and refactoring to the extent that XP does.
I haven't tried another incremental design approach with XP because they seem to interact clumsily with XP's short iterations. The design sessions seem like they would be too short and small to create a cohesive architecture on their own. Without XP's focus on simple design and merciless refactoring, a single design might not evolve.
Another alternative is to design everything up-front. This could work in an environment with very few requirements changes (or a prescient designer), but it's likely to break down with XP's adaptive plans and tiered planning horizons.
"Is Design Dead?" [Fowler 2000], online at http://www.martinfowler.com/articles/designDead.html, discusses evolutionary design from a slightly skeptical perspective.
"Continuous Design" [Shore 2004a] discusses my experiences with difficult challenges in incremental design, such as internationalization and security. It is available at http://www.martinfowler.com/ieeeSoftware/continuousDesign.pdf.