AoAD2 Practice: Simple Design

Book cover for “The Art of Agile Development, Second Edition.”

Second Edition cover

This is a pre-release excerpt of The Art of Agile Development, Second Edition, to be published by O’Reilly in 2021. Visit the Second Edition home page for information about the open development process, additional excerpts, and more.

Your feedback is appreciated! To share your thoughts, join the AoAD2 open review mailing list.

This excerpt is copyright 2007, 2020, 2021 by James Shore and Shane Warden. Although you are welcome to share this link, do not distribute or republish the content without James Shore’s express written permission.

Simple Design

Audience
Programmers

Our code 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, author of “The Little Prince”

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 programmers often stop to ask themselves, “What’s the simplest thing that could possibly work?” They seem to be obsessed with simplicity. Rather than anticipaating changes and providing extensibility hooks and plug-in points, they create a simple design that anticipates as little as possible, as cleanly as possible. Counterintuitively, 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? Isn’t the key to success to maximize reuse by anticipating future changes?

I’ll let Erich Gamma, coauthor of Design Patterns: Elements of Reusable Software, answer these questions. In the following excerpt, Gamma is interviewed by Bill Venners. [Venners 2005]

Venners: [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 [Extreme Programming] philosophy.

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 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 [design] 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 [in 1995], 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.

Venners: So what do the XP guys do first, if they don’t use patterns? They just write the code?

Gamma: They write a test.

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?

Gamma: Or when there’s a new requirement. I really like the 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.

Allies
Reflective Design
Incremental Design

Traditional approaches to design focus on creating extensible designs. But, as Erich Gamma says, it’s expensive to speculate about flexibility. The Agile approach is to create simple designs, with no speculation. It combines with reflective design and incremental design to allow your design to evolve in any direction, regardless of how or when customers change their minds.

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.

When, not if, I need to change this decision, how hard will it be?

Whenever I make a design decision, I always ask myself this question: “When, not if, I need to change this decision, how hard will it be?” The following techniques will help you keep your code simple and change costs low.

You Aren’t Gonna Need It

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’s necessary for what the software does today. If not, well... you aren’t gonna need it. Your design could change. Your customers’ minds could change.

Similarly, remove code that’s no longer in use. You’ll make the design smaller, simpler, and easier to understand. If you need the code in the future, you can always get it out of version control. For now, it’s a maintenance burden.

Think of it this way: when it’s time to implement a new feature, would you rather deal with an existing design that’s wrong, or no existing design at all? When I raise this question with audiences, they overwhelmingly prefer the second option. It’s far easier to add code than to rip out and replace code that’s wrong.

When you speculate about the future, you make mistakes. You create code that has to be ripped out and replaced. All too often, those incorrect assumptions end up with their tendrils extending throughout the code, make the cost of removing them all the more difficult. It’s better not to speculate in the first place.

Once and Only Once

Once and only once is a surprisingly powerful design guideline. As Martin Folwer said: [Venners 2002]

One of the things I’ve been trying to do is look for simpler [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 and Thomas 1999] 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.

“Once and only once” isn’t just about removing duplicated code, though. It’s about giving every concept that’s important to your code a home. Think of it this way:

Express every concept Once. And only once.1

1Thanks to Andrew Black for this insight.

Every piece of knowledge must have a single, unambiguous, authoritative representation.

Or, as [Hunt and Thomas 1999] 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 those concepts with primitive data types—an approach called “Primitive Obsession”—create a new type.

For example, a company creating an online storefront represented money with floating point numbers. Instead, they could have created a Currency class. In JavaScript, it would look like this:

class Currency {
  constructor(amount)
    this._amount = amount;
  }

  toNumber() {
    return this._amount;
  }

  equals(currency) {
    return this._amount === currency._amount;
  }
}

At first, this seems wasteful. It’s just a simple wrapper over an underlying data type, except now with added overhead. Not only that, it seems like it increases complexity by adding another class.

But this sort of simple value type turns out to be immensely effective at enabling the “once and only once” principle. Now, any code that’s related to currency has an obvious place to live: inside the Currency class. If somebody needs to implement some new code, they look there first to see if it’s already implemented. And when something about the concept needs to change—say, foreign currency conversion—there’s one obvious place to implement that change.

The alternative isn’t pretty. That online storefront? It turned out that floating point math wasn’t a great choice. They got themselves into a situation where, when line item refunds and taxes were involved, they couldn’t generate a refund that matched the original invoice. (Whoops.) They had to engage in a multi-month process of finding everything that related to currency and changing it to use fixed-point math. True story.

Bet they wish they had expressed the Currency concept once. (And only once.) They could have changed the implementation of their Currency class and called it a day.

When, not if, you need to change a design decision, how hard will it be?

Coupling and Cohesion

Coupling and cohesion are ancient software design ideas that extend back to Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. [Yourdon and Constantine 1975] (ch. 6-7) They’re no less powerful for their age. Both terms refer to the relationship between concepts in your code.2

2I’ve updated the definitions slightly. The original definition discusses “modules,” not “concepts.”

Two parts of the code are coupled when they a change to one necessitates a change to the other. To continue the Currency example, a function to convert currency to a string would be coupled to the data type used for currency.

Two concepts are cohesive when they’re physically close together in your source files. For example, if the function to convert currency to a string was in a Currency class, along with the underlying data type, they would be highly cohesive. If the function was in a utility module in a completely different directory, they would have low cohesion.

The best code has low coupling and high cohesion. In other words, changing the code for one concept doesn’t require you to change code for any other concept: changing the Currency data type doesn’t require changing the authentication code, or the refund logic. At the same time, when two parts of the code are coupled, they’re highly cohesive: if you change the Currency data type, everything that’s affected is in the same file, or at least in the same directory.

When you make a design decision, step back from design patterns and architectural principles and language paradigms for a moment. Ask yourself a simple question: When, not if, somebody changes this code, will the other things they need to change also be obvious? The answer comes down to coupling and cohesion.

Third-Party Components

Third-party components—libraries, frameworks, and services—are a common cause of problems. They tend to extend their tendrils throughout your code. When, not if, you need to replace or upgrade the component, the changes can be difficult and far-reaching.

To prevent this problem, isolate third-party components behind an interface you control. This is called an adapter or a wrapper. (In object-oriented languages, you can use the Adapter pattern. [Gamma et al. 1995]) Rather than using the component directly, your code uses the interface.

In addition to making your code resilient to third-party changes, your adapter can also present an interface customized for your needs, rather than imitating the third-party interface, and you can extend it with additional features as needed.

For example, when I wrote a wrapper for the Recurly billing service, I didn’t expose a method for Recurly’s subscriptions endpoint. Instead, I wrote isSubscribed(), which called the endpoint, parsed its XML, looped through the subscriptions, and converted the many possible subscription status flags into a simple boolean result.

Create your adapters incrementally. Instead of supporting every feature of the component you’re adapting, support only what you need today, focusing on providing an interface that matches the needs of your code. This will make the adapter cheaper to create and make it easier to change when (not if) you need to replace the underlying component in the future.

Some components—particularly frameworks—want to “own the world” and are difficult to hide behind adapters. For this reason, I prefer to build my code using simple libraries, with narrowly-defined interfaces, rather than one big do-everything framework. In some cases, though, a framework is the best choice. For frameworks that require you to extend base classes, you can create an adapter by building your own base classes that extends the frameworks’ classes, rather than extending the framework directly.

Alternatively, you can choose not to wrap the third-party components. This makes the most sense when the component is pervasive and stable, such as core language frameworks. You can make this decision on a case-by-case basis: for example, I’ll use the .NET String class directly, without an adapter, but I’ll use an adapter to isolate .NET’s cryptography libraries—not because I think they’ll change, but because they’re complicated, and an adapter will allow me to hide and centralize that complexity.

Fail Fast

One of the pitfalls of simple design is that your design will be incomplete. If you’re following the YAGNI principle (“You Aren’t Gonna Need It”), there will be some scenarios that your code just isn’t capable of handling. For example, you could write a currency rendering method that isn’t aware of non-US currencies yet, because your code currently renders everything in US dollars. But later, when you support more currencies, that gap could result in a bug.

To prevent these gaps from becoming a problem down the road, 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 their tests to fail. For example, you could add an assertion to fail when the Currency class is asked to render a non-US currency.

Most language have some sort of run-time assertions built in. I like to write my own assertion module, though, and give it expressive functions (with good error messages) such as ensure.notNull(), ensure.unreachable(), ensure.impossibleException(), and so forth. I have them throw an exception when the assertion is violated.

Some people worry that failing fast will make their code more brittle, but the opposite is actually true. By failing fast, you make errors more obvious, which means you catch them before they go into production. As a safety net, though, to prevent your software from crashing outright, you can add a top-level exception handler that logs the error and recovers.

Fail fast code works best when combined with sociable tests (see “Write Sociable Tests” on p.XX), because sociable tests will trigger the fail fast checks, allowing you to find gaps more easily. Isolated tests require your tests to make assumptions about the behavior of dependencies, and it’s easy to assume a dependency will work when it actually fails fast.

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, but run them past other team members first.

Names are one of your most powerful tools for self-documenting code. Be sure to use names that clearly reflect the intent of your variables, functions, classes, modules, and other entities. When a function has a lot of moving parts, use the Extract Function refactoring [Fowler 2018] to name each piece. When a conditional is hard to understand, use functions or intermediate variables to name each piece of the conditional.

Note that I didn’t say anything about comments. Comments aren’t bad, exactly, but they’re a crutch. Try to find ways to make your code so simple and expressive that comments aren’t needed.

Allies
Pair Programming
Mob Programming
Refactoring
Collective Code Ownership

Good names and simple code are hard. Three things make them easier: first, pairing or mobbing give you more perspectives and more ideas for good names. If you’re having trouble thinking of a good name, or you think some code your driver wrote is unclear, discuss the situation and try to find a better way to express it.

Second, you can always refactor. Give it your best shot now, and when you come back to it later, refactor it to be even better.

Third, take advantage of collective code ownership. When you see code that isn’t clear, figure out what the person who wrote it was trying to say, then refactor to make that intent obvious.

Limit Published Interfaces

Published interfaces reduce your ability to make changes. Once an interface is published to people outside your team, changing that interface typically requires a lot of expense and effort. You have to be careful not to break anything that they’re relying upon.

Some teams approach design as if every public method was also a published interface. This approach assumes that, once defined, a public method should never change. To be blunt, this is a bad idea: it prevents you from improving your design over time. A better approach is to change non-published interfaces whenever needed, updating callers accordingly.

If your code is used outside your team, then you do need published interfaces. Each one is a commitment to a design decision that you may wish to change in the future, so minimize the number of interfaces you publish. For each one, consider whether the benefit is worth the cost. Sometimes it will be, but it’s a decision to be made carefully. Postpone the decision as long as possible to allow your design to improve and settle.

In some organizations—including, famously, Google—teams have the ability to change other teams’ code. When they want to change an interface that‘s only published within the organization, they can easily do so.

In some cases, as with teams creating a library for third-party use, the entire purpose of the product is to provide a published interface. In that case, design your interface carefully, up front, rather than using evolutionary design. The smaller the interface, the better—it’s much easier to add to your interface than to remove mistakes.

Performance Optimization

Modern computers are complex. Even reading a single line of a file from a disk requires the coordination of the CPU, multiple levels of CPU cache, the operating system kernel, a virtual file system, a system bus, the hard drive controller, the hard drive cache, OS buffers, system buffers, and scheduling pipelines. Every component exists to solve a problem, and each has certain tricks to squeeze out performance. Is the data in a cache? Which cache? How’s your memory aligned? Are you reading asynchronously or are you blocking? There are so many variables, it’s nearly impossible to predict the performance of any single method.

The days in which a programmer could accurately predict performance by counting instruction cycles are long gone, yet some programmers still approach performance with this simplistic, brute-force mindset. They make random guesses about performance based on folklore and 20-line performance tests, flail around writing every micro-optimization they can think of, leave a twisty mess in the code, and take a long lunch.

Your intuition about performance is almost always going to be wrong.

In other words, your intuition about performance is almost always going to be wrong.

The only way to optimize modern systems is to take a holistic approach. You have to measure the real-world performance of the code, find the hot spots, and optimize from there. Don’t guess. Don’t make assumptions. Just profile the code.

String buffers, function calls, and boxing/unboxing—the most common bugaboos of the micro-optimizer—are almost always not the issue. Most of the time, your performance will be dominated by the network, database, or file system. If not, it’s likely to be an O(n²) or worse algorithm. Rarely, it will be thread contention or non-sequential memory access inside a tight loop. But the only way to be sure is to measure real-world performance. Don’t guess. Profile, profile, profile.

In the meantime, ignore the micro-optimization tricks. When (not if) you need to change your code—whatever the reason—it will be easier to do if it’s simple and straightforward.

Questions

What if we know we’re going to need a story? Shouldn’t we put in a design hook for it?

Your plan can change at any time. Unless the story is part of your current task plan, don’t put the hook in. The story could disappear from the plan, leaving you stuck with unnecessary complexity.

More importantly, evolutionary design actually reduces the cost of changes over time, so the longer you wait to make the change, the cheaper it will be.

What if ignoring a story will make it harder to implement in the future?

A simple design should make arbitrary changes possible by reducing duplication and reducing the impact of changes. If ignoring a potential story could make it more difficult, look for ways to eliminate that risk without explicitly coding support for the story. “Risk-Driven Refactoring” on p.XX has more details.

Prerequisites

Allies
Refactoring
Reflective Design
Incremental Design
Collective Code Ownership
Pair Programming
Mob Programming

Simple design requires continuous improvement through refactoring, reflective design, and incremental design. Without them, 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 practice reminds us, it’s a lot easier to create complex designs than simple designs. Don’t pretend “simple” means the code that’s fastest or easiest to implement.

Collective code ownership and pairing or mobbing, though not strictly necessary for simple design, will help your team devote the brainpower needed to create truly simple designs.

Indicators

When you create simple designs:

  • Your team doesn’t write code in anticipation of future stories.

  • Your team finishes work more quickly, because they don’t build things that aren’t needed today.

  • Your design supports arbitrary changes easily.

  • Although new features might require a lot of new code, changes to existing code are localized and straightforward.

Alternatives and Experiments

Most people still consider the advice Erich Gamma now disavows to be the best practice for design: “The key to maximizing reuse [and design quality] lies in anticipating new requirements and changes to existing requirements, and in designing your systems so they can evolve accordingly.”

Ally
Reflective Design

I call this “predictive design,” in contrast to reflective design, which I’ll discuss next. Although many teams have had success using predictive design, it relies on accurately anticipating new requirements and stories. If your expectations are too far off, you might have to rewrite a lot of code that was based on bad assumptions. Some of those rewrites might affect so much code that they can’t be done economically, resulting in long-term cruft in your codebase.

Generally, I’ve found the simple design techniques described in this practice to work better than predictive design, but you can combine the two. If you do use a predictive design approach, it’s best to hire programmers 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 and Thomas 1999] contains a wealth of design information that will help you create simple, flexible designs. Practices of an Agile Developer [Subramaniam and Hunt 2006] is its spiritual successor, offering similarly pithy advice, though with less emphasis on design and coding.

Prefactoring [Pugh 2005] also has good advice for creating simple, flexible designs.

“Fail Fast” [Shore 2004b] discusses that concept in more detail.

Share your feedback about this excerpt on the AoAD2 mailing list! Sign up here.

For more excerpts from the book, or to get a copy of the Early Release, see the Second Edition home page.

If you liked this entry, check out my best writing and presentations, and consider subscribing to updates by email or RSS.