People trying to describe Toyota's product development process seem to have settled on "set-based concurrent engineering" as a term for one key part. They see the approach as set-based because it's built around the idea of finding a solution as the intersection of a set of feasible choices, and it's concurrent because teams are trying to work "all at once," not in a sequential, hand-off style.
You probably know the game of 20 Questions. Let's try it "point-based":
I'm thinking of
Instead, let's use a "set-based" approach:
I'm thinking of
The point-based approach proposes a solution, and then iterates by revising to a different point. In real design, there's more feedback than in the first game above, of course. But the point solution is in effect treated as "the right answer." If we find out it's not right, we move to a different point.
A set-based approach takes a different path. Instead of moving to a solution right away, it starts with options open, then narrows in on a solution.
Why might we want to take the set-based approach? Because it lets us:
We'll look at three ways that software can use a set-based approach.
1. Consciously explore alternatives
One approach is to identify several alternatives, and spend energy to spike and/or analyze the choices. For example, should we use C++, Java, or C#? Should we use an object database or a relational database? Quicksort or insertion sort or radix sort or ...?
In lean product development, they use tradeoff curves to record their findings. (See The Toyota Product Development System for more.) That's inspired me to produce the following diagrams. See the wikipedia article on sorting algorithms for many more algorithms and a fuller discussion.
Here's another view of the same information:
You might make a matrix comparing several choices: (fake values; you're on your own here:)
In each of these cases, we spend time identifying and exploring the alternatives, then strive for a clear, succinct way to present the tradeoffs involved.
These summaries can help you narrow in. For example, we might decide early on that we need sorting. A later decision might highlight that stable sorting is important. This would cut down the number of algorithm options. A later decision might highlight the need for consistent performance, cutting our options even more, until we're to the point where we can choose an algorithm.
This approach, of exploring, recording, and narrowing alternatives, feels closest in spirit to the Toyota product-development approach.
2. Consciously enable alternatives
A second approach is to use modularity: create designs that explicitly enable different alternatives. The book Design Rules, Volume 1: The Power of Modularity, by Baldwin and Clark, explores modularity from a non-software-specific point of view; Design Patterns (Gamma et al.), Pattern-Oriented Software Architecture (Buschmann et al.), and many other pattern and software books consider modularity from a software perspective.
Many examples of modularity are based around the ideas of abstraction and of an interface: two parts make a commitment to how they will "talk" to each other, but no commitment about what happens beyond the interface. Consider these examples:
Each of these patterns represents a "move" that is consciously leaving room for a set of alternative designs.
3. Incremental design as not blocking alternatives
Gordon Bell said, "The cheapest, fastest, and most reliable components are those that aren't there." Simple design (in the XP sense) suggests, "Don't add anything until you have a failing test that demonstrates you need it."
Each part you add removes flexibility - it either interferes with what you might have done instead, or it becomes dead weight, dragged along even though nothing needs it. And yet, if we don't add parts, our software doesn't do anything.
For example, you've decided on a persistence approach. You could "build the persistence layer," mapping out all objects from all the tables you already have. This creates a "point-based" solution - this is the way persistence works. It creates a drag, from the effort spent on objects you don't need yet. Each of those objects will have to be tested, refactored, and maintained, against that possible day in the future when all that work may finally pay off.
Instead, an incremental design might commit us a little now, but it leaves to the future the set of all possible ways to implement the objects based on the current structure. If we get an idea for a better approach, unmapped objects are free to use it when it's time to map them. (And if the whole approach becomes rejected, we only pay to change objects that were already needed.)
I think of a simple design as set-based in this sense: Consider the program we have, doing just enough to support the current set of features. Some aspects of this program are a commitment to a particular design. But there is a set of programs out there - all those programs that extend the current program in a way compatible with the current design. By using "just enough design" - not implementing parts not needed to support functionality, we don't eliminate those programs that would evolve in a different way to support those future parts. By contrast, if we fully elaborate an implementation to support our guesses about future directions, we in effect are betting on a point solution, not a set of possible evolutions.
4. Carry Forward with Multiple Implementations
Kent Beck wrote:
This approach goes further than the first one - rather than evaluate the alternatives and make a guess, we carry forward on multiple implementations, thus seeing the real impact of our decisions.
There are several different design approaches that yield aspects of a set-based design:
By applying these tools, you can help match the level of commitment to your solution to the level of understanding you have in the constraints surrounding it.
[November, 2006. Updated 12-20-2006 with Kent Beck's fourth strategy.]
Copyright 1994-2010, William C. Wake - William.Wake@acm.org