The Art of Agile Development: Simple Design
September 23, 2010
The second edition is now available! The Art of Agile Development has been completely revised and updated with all new material. Visit the Second Edition page for more information, or buy it on Amazon.
- Next: Incremental Design
- Previous: Refactoring
- Up: Chapter 9: Developing
Full Text
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.
Simple Design
- Audience
- Programmers
Our design is easy to modify and maintain.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. —Antoine de Saint-Exupéry
Any intelligent fool can make things bigger, more complex and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction. —Albert Einstein.
When writing code, agile developers often stop to ask themselves, "What is the simplest thing that could possibly work?" They seem to be obssessed with simplicity. Rather than anticipating changes and providing extensibility hooks and plug-in points, they create a simple design that anticipates as little as possible, as cleanly as possible. Unintuitively, this results in designs that are ready for any change, anticipated or not.
This may seem silly. How can a design be ready for any change? Isn't the job of a good designer or architect to anticipate future changes and make sure the design can be extended to support them? Doesn't Design Patterns: Elements of Reusable Software say that the key to maximizing reuse is to anticipate changes and design accordingly?
I'll let Erich Gamma, coauthor of Design Patterns, answer these questions. In the following excerpt, Gamma is interviewed by Bill Venners.1
1"Erich Gamma on Flexibility and Reuse: A Conversation with Erich Gamma, Part II", http://www.artima.com/lejava/articles/reuse.html, accessed 6 April 2007.
Bill Venners: The GoF book [Design Patterns] says, "The key to maximizing reuse lies in anticipating new requirements and changes to existing requirements, and in designing your systems so they can evolve accordingly. To design a system so that it's robust to such changes, you must consider how the system might need to change over its lifetime. A design that doesn't take change into account risks major redesign in the future." That seems contradictory to the XP philosophy.
Erich Gamma: It contradicts absolutely with XP. It says you should think ahead. You should speculate. You should speculate about flexibility. Well yes, I matured too and XP reminds us that it is expensive to speculate about flexibility, so I probably wouldn't write this exactly this way anymore. To add flexibility, you really have to be able to justify it by a requirement. If you don't have a requirement up front, then I wouldn't put a hook for flexibility in my system up front.
But I don't think XP and patterns are conflicting. It's how you use patterns. The XP guys have patterns in their toolbox, it's just that they refactor to the patterns once they need the flexibility. Whereas we said in the book ten years ago, no, you can also anticipate. You start your design and you use them there up-front. In your up-front design you use patterns, and the XP guys don't do that.
Bill Venners: So what do the XP guys do first, if they don't use patterns? They just write the code?
Erich Gamma: They write a test.
Bill Venners: Yes, they code up the test. And then when they implement it, they just implement the code to make the test work. Then when they look back, they refactor, and maybe implement a pattern?
Erich Gamma: Or when there's a new requirement. I really like flexibility that's requirement driven. That's also what we do in Eclipse. When it comes to exposing more API, we do that on demand. We expose API gradually. When clients tell us, "Oh, I had to use or duplicate all these internal classes. I really don't want to do that," when we see the need, then we say, OK, we'll make the investment of publishing this as an API, make it a commitment. So I really think about it in smaller steps, we do not want to commit to an API before its time.
Beck on Simplicity
In the first edition of Extreme Programming Explained, Kent Beck described simple design as code that passes its tests and meets four guidelines, with the earlier guidelines taking precedence over the later ones.
The system (code and tests together) must communicate everything you want to communicate.
The system must contain no duplicate code. (1 and 2 together constitute the Once and Only Once rule).
The system should have the fewest possible classes.
The system should have the fewest possible methods.
In the second edition, he rephrased the advice.
Appropriate for the intended audience. It doesn't matter how brilliant and elegant a piece of design is; if the people who need to work with it don't understand it, it isn't simple for them.
Communicative. Every idea that needs to be communicated is represented in the system. Like words in a vocabulary, the elements of the system communicate to future readers.
Factored. Duplication of logic or structure makes code hard to understand and modify.
Minimal. Within the above three constraints, the system should have the fewest elements possible. Fewer elements means less to test, document, and communicate.
Simple, not simplistic.
Simple doesn't mean simplistic. Don't make boneheaded design decisions in the name of reducing the number of classes and methods. A simple design is clean and elegant, not something you throw together with the least thought possible. Here are some points to keep in mind as you strive for simplicity:
You Aren't Gonna Need It (YAGNI)
This pithy XP saying sums up an important aspect of simple design: avoid speculative coding. Whenever you're tempted to add something to your design, ask yourself if it supports the stories and features you're currently delivering. If not, well... you aren't gonna need it. Your design could change. Your customers' minds could change.
- Ally
- Version Control
Similarly, remove code that's no longer in use. You'll make the design smaller, simpler, and easier to understand. If you need it again in the future, you can always get it out of version control. For now, it's a maintenance burden you don't need.
We do this because excess code makes change difficult. Speculative design, added to make specific changes easy, often turns out to be wrong in some way, which actually makes changes more difficult. It's usually easier to add to a design than to fix a design that's wrong. The incorrect design has code that depends on it, sometimes locking bad decisions in place.
Once and Only Once
Once and only once is a surprisingly powerful design guideline. As Martin Fowler said:2
2http://www.artima.com/intv/principlesP.html.
One of the things I've been trying to do is look for simpler or rules underpinning good or bad design. I think one of the most valuable rules is avoid duplication. "Once and only once" is the Extreme Programming phrase. The authors of The Pragmatic Programmer [Hunt & Thomas] use "don't repeat yourself," or the DRY principle.
You can almost do this as an exercise. Look at some program and see if there's some duplication. Then, without really thinking about what it is you're trying to achieve, just pigheadedly try to remove that duplication. Time and time again, I've found that by simply removing duplication I accidentally stumble onto a really nice elegant pattern. It's quite remarkable how often that is the case. I often find that a nice design can come from just being really anal about getting rid of duplicated code.
There's even more to this idea than removing duplication. Think of it this way:
Express every concept once. (And only once).3
3Thanks to Andrew Black for this insight.
In other words, don't just eliminate duplication; make sure that every important concept has an explicit representation in your design. As [Hunt & Thomas] phrase their DRY Principle: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
An effective way to make your code express itself once (and only once) is to be explicit about core concepts. Rather than expressing these concepts with an primitive data type, create a new type. For example, rather than representing dollar amounts with a decimal
data type, create a Dollars
class. (See Example.)
Example. Simple value type
public class Dollars { private decimal _dollars; public Dollars(decimal dollars) { _dollars = dollars; } public decimal AsDecimal() { return _dollars; } public boolean Equals(object o) {...} }
Although using basic data types may seem simpler—it's one less class, after all—it actually makes your design more complex. Your code doesn't have a place for the concept. As a result, every time someone works with that concept, the code may need to reimplement basic behavior, such as string parsing and formatting, which results in widespread duplication. This duplication will likely be only fragments of code, but the net weight of those fragments will make your code hard to change. For example, if you decide to change the display of dollar amounts—perhaps you want negative amounts to be red—you must find every little fragment of formatting code and fix it.
Instead, make sure that every concept has a home. Don't generalize; just make sure the basic concept has an explicit representation. Over time, as needed, add code (such as formatting and parsing) to your type. By starting with a simple but explicit representation of the concept, you provide a location for those future changes to congregate. Without it, they will tend to accumulate in other methods and lead to duplication and complex code.
Self-Documenting Code
Simplicity is in the eye of the beholder. It doesn't matter much if you think the design is simple; if the rest of your team or future maintainers of your software find it too complicated, then it is.
To avoid this problem, use idioms and patterns that are common for your language and team. It's okay to introduce new ideas, too, but run them past other team members first. Be sure to use names that clearly reflect the intent of your variables, methods, classes, and other entities.
- Ally
- Pair Programming
Pair programming will help you create simple code. If you have trouble understanding something your partner wrote, discuss the situation and try to find a better way to express the concept. Before you use a comment to explain something, ask your partner how to make the code express its intent without needing a comment.
Comments aren't bad, but they are a sign that your code is more complex than it needs to be. Try to eliminate the need for comments when you can. You can't just arbitrarily delete comments, of course—first make the code so expressive that the comments no longer add value.
Isolate Third-Party Components
A hidden source of duplication lies in calls to third-party components. When you have these calls spread throughout your code, replacing or augmenting that component becomes difficult. If you discover that the component isn't sufficient for your needs, you could be in trouble.
To prevent this problem, isolate your third-party components behind an interface that you control. Ask yourself, "When I need to upgrade or change this component, how hard will it be?" In object-oriented languages, consider using the Adapter pattern [Gamma et. al.] rather than instantiating third-party classes directly. For frameworks that require that you extend their classes, create your own base classes that extend the framework classes, rather than extending the classes directly.
Isolating third-party components also allows you to extend the features of the component and gives you a convenient interface to write tests against if you need to.
Create your adapter incrementally. Rather than supporting every feature of the component in your adapter, support only what you need today. Write the adapter's interface to match your needs, not the interface of the component. This will make it easier to use and to replace when necessary.
Isolating third-party components reduces duplication at the cost of making your code slightly more complex. Some components, such as Java's J2SE or the .NET framework, are so pervasive that isolating them makes little sense. Make the decision to isolate common components according to the risk that you'll need to replace or augment that component. For example, I would use the Java or .NET String
class directly, without an adapter, but I might consider isolating .NET's cryptography libraries or elements of the J2EE framework.
Limit Published Interfaces
Published interfaces limit your ability to make changes. Once the public interface to a class or other module is published so that people outside the team may use it, changing it requires great expense and effort. Because other people might be using the interface, changing it can sabotage their efforts.
Some teams approach design as if every public interface were also a published interface. This internal publication assumes that, once defined, a public interface should never change. This is a bad idea—it prevents you from improving your design over time. A better approach is to change your nonpublished interfaces whenever you need, updating callers accordingly.
If your code is used outside your team, then you do need published interfaces. Each one, however, is a commitment to a design decision that you may wish to change in the future. Minimize the number of interfaces you expose to the outside world and ask if the benefit of having other teams use your code is really worth the cost.4 (Sometimes it is, but don't automatically assume so.) Postpone publishing interfaces as long as possible to allow your design to improve and settle.
4[Brooks] estimated that making code externally reusable increases costs threefold. That estimate probably doesn't apply to modern development, but there's still a nontrivial cost associated with creating reusable components. "Object-oriented" doesn't mean "automatic reuse," despite early claims to the contrary.
In some cases, as with teams creating a library for third-party use, the entire purpose of the project is to create a published interface. In that case, your API is your product. Still, the smaller the interface, the better—it's much easier to add new elements to your API than to remove or change incorrect elements. Make the interface as small as is practical.
As Erich Gamma said in his interview, "When it comes to exposing more API [in Eclipse, the open source Java IDE], we do that on demand. We expose API gradually... when we see the need, then we say, okay, we'll make the investment of publishing this as an API, make it a commitment. So I really think about it in smaller steps, we do not want to commit to an API before its time. "
When developing a library, you can develop your interface incrementally and then freeze it only when you release. Still, it's probably a good idea to think ahead to future changes and consider whether anything about the API you're about to publish will make those changes difficult.
Fail Fast
One of the pitfalls of simple design is that your design will be incomplete. Some elements won't work because no one has needed them before.
To prevent these gaps from being a problem, write your code to fail fast. Use assertions to signal the limits of your design; if someone tries to use something that isn't implemented, the assertion will cause his tests to fail.
Sometimes you can have more expressive assertions by writing your own assertion facility, rather than using your language's built-in facility. I like to create a class called
Assert
(Assume
andRequire
are good synonyms ifAssert
is unavailable) and implement class (static) methods such asnotNull(object)
,unreachableCode()
andimpossibleException(exception)
.
Questions
What if we know we're going to need a feature? Shouldn't we put in a design hook for it?
In XP, the plan can change every week. Unless you're implementing the feature that very week, don't put the hook in. The plan could change, leaving you stuck with unneeded code.
Plus, if you're using incremental design and architecture properly, your code will actually be easier to modify in the future than it is today. Saving the change for later will save time and money.
What if ignoring a feature will make it harder to implement in the future?
A simple design should make arbitrary changes possible by reducing duplication and limiting the scope of changes. If ignoring a potential feature could make it more difficult, you should look for ways of eliminating that risk without explicitly coding support for the feature. Incremental Design And Architecture, later in this chapter, has more about risk-driven architecture.
Results
When you create simple designs, you avoid adding support for any features other than the ones you're working on in the current iteration. You finish work more quickly as a result. When you use simple design well, your design supports arbitrary changes easily. Although new features might require a lot of new code, changes to existing code are localized and straightforward.
Contraindications
Simple design requires continuous improvement through refactoring and incremental design and architecture. Without it, your design will fail to evolve with your requirements.
Don't use simple design as an excuse for poor design. Simplicity requires careful thought. As the Einstein quote at the beginning of this section says, it's a lot easier to create complex designs than simple ones. Don't pretend "simple" means "fastest" or "easiest."
Pair programming and collective code ownership, though not strictly necessary for simple design, will help your team devote the brainpower needed to create truly simple designs.
Alternatives
Until recently, the accepted best practice in design followed the advice Erich Gamma now disavows: "The key to maximizing reuse lies in anticipating new requirements and changes to existing requirements, and in designing your systems so they can evolve accordingly."
A team can have success with this approach, but it depends on how well they anticipate new requirements. If the team's expectations are too far off, they might need to rewrite a lot of code that was based on bad assumptions. Some changes may affect so much code that they're just considered impossible. If you follow this approach, it's best to hire designers who have a lot of experience in your specific industry. They're more likely to correctly anticipate changes.
Further Reading
Martin Fowler has a collection of his excellent IEEE Design columns online at http://www.martinfowler.com/articles.html#IDAOPDBC. Many of these columns discuss core concepts that help in creating a simple design.
The Pragmatic Programmer: From Journeyman to Master [Hunt & Thomas] contains a wealth of design information that will help you create simple, flexible designs. Practices of an Agile Developer [Subramaniam & Hunt] is its spiritual successor, offering similarly pithy advice, although with less emphasis on design and coding.
Prefactoring [Pugh] also has good advice for creating simple, flexible designs.
"Fail Fast" [Shore 2004b] discusses that concept in more detail. It is available at http://www.martinfowler.com/ieeeSoftware/failFast.pdf.
- Next: Incremental Design
- Previous: Refactoring
- Up: Chapter 9: Developing