|One of the ideas in lean product development is the notion of set-based concurrent engineering: considering a solution as the intersection of a number of feasible parts, rather than iterating on a bunch of individual "point-based" solutions. This lets several groups work at the same time, as they converge on a solution.|
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 something.
Is it animal, vegetable or mineral? Animal.
Is it a schnauzer? No.
Is it a parakeet? No.
Is it a goldfish? No.
Mini-lop rabbit? No.
Blue-tailed skink? You got it.
Instead, let's use a "set-based" approach:
I'm thinking of something.
Is it animal, vegetable or mineral? Animal.
Is it bigger than a breadbox? No.
Is it a mammal? Yes.
Is it a dog? Yes.
Is it curly-haired? Yes.
Is it a standard poodle? You got it.
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:
- defer decisions until the "last responsible moment," when we have the best information;
- explore many possible designs at the same time; and
- converge designs more quickly.
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.
Comparison-Based Sorting Algorithms
|Algorithm||O(n)||O(n log n)||O(n1.5)||O(n2)||Stable?|
Key: B=best-case, E=expected-case, W=worst-case.
Default choices are italicized.
Here's another view of the same information:
You might make a matrix comparing several choices: (fake values; you're on your own here:)
|Hibernate||Low||Med||Low||Med to High||Med|
|Custom layer||Med||High||Med||Low to High||High|
|Toplink||Med||Lo||Hi||Med to High||Med|
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:
- Service-oriented architecture. For example, two systems communicate via SOAP, with no commitment about what technology or tool will be used on either side.
- Network protocols.
- Plug-in architecture.
- Layered architecture.
- Adapter design pattern.
- Bridge (driver) design pattern. For example, we commit to using a JDBC driver but don't commit to whether we'll use SQL Server, MySQL, or Oracle.
- Abstract class.
- Machine-independent language.
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:
I think there is another strategy that also fits the bill–duplicate implementation. The example that brought this home to me was a client of [someone]’s who reported that they were trying to decide between an HTML interface or a rich client interface. [His] advice was to implement both, keeping them in sync every iteration until it was clear which one was most valuable. The client said that they couldn't stand the idea of wasting all that effort, so they picked early and later regretted it.
This is the kind of strategy that is only possible when you have slack programming capacity, enough so you don't mind applying some to risk reduction.
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:
- exploring alternatives (and recording and using the results of that exploration)
- designing in mechanisms that support alternative designs
- committing to the minimal level of current design, to leave room for a variety of future designs
- carry forward with multiple implementations
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.
- Baldwin and Clark. Design Rules, Volume 1: The Power of Modularity.
- Buschmann, Frank, et al. Pattern-Oriented Software Architecture, Volume 1: A System of Patterns.
- Gamma, Erich, et al. Design Patterns. Addison-Wesley, 1995.
- Morgan, James and Jeffrey Liker. The Toyota Product Development System, Productivity Press, 2006.
- Poppendieck, Mary and Tom Poppendieck. Implementing Lean Software Development. Addison-Wesley, 2006. [I tried to express some of these ideas in a quote they included there.]
- Thimbleby, Harold. "Delaying Commitment," IEEE Software, V5 #3, May, 1988. [Many authors have talked about modularity in software for a while; this paper explicitly calls out delaying commitment as a design strategy.]
[November, 2006. Updated 12-20-2006 with Kent Beck’s fourth strategy. Updated 1/4/2014 to add link to more resources.]