3
more moves ahead. This takes time to learn. Maybe this is the reason why programming genius may show
at an early age, whereas software design genius tends to take more time to ripen.
In addition to being a puzzle for beginners, the combinatorial nature of design decisions is a major source
of trouble for library writers. To implement a useful library of designs, the library designer must classify
and accommodate many typical situations, yet still leave the library open-ended so that the application
programmer can tailor it to the specific needs of a particular situation.
Indeed, how can one package flexible, sound design components in libraries? How can one let the user
configure these components? How does one fight the "evil multiplicity" of design with a reasonably sized
army of code? These are the questions that the remainder of this chapter, and ultimately this whole book,
tries to answer.
1.2 The Failure of the Do-It-All Interface
Implementing everything under the umbrella of a do-it-all interface is not a good solution, for several
reasons.
Some important negative consequences are intellectual overhead, sheer size, and inefficiency. Mammoth
classes are unsuccessful because they incur a big learning overhead, tend to be unnecessarily large, and
lead to code that's much slower than the equivalent handcrafted version.
But maybe the most important problem of an overly rich interface is loss of static type safety. One essential
purpose of the architecture of a system is to enforce certain axioms "by design"—for example, you cannot
create two Singleton objects (see Chapter 6
) or create objects of disjoint families (see Chapter 9). Ideally, a
design should enforce most constraints at compile time.
In a large, all-encompassing interface, it is very hard to enforce such constraints. Typically, once you have
chosen a certain set of design constraints, only certain subsets of the large interface remain semantically
valid. A gap grows between syntactically valid and semantically valid uses of the library. The programmer
can write an increasing number of constructs that are syntactically valid, but semantically illegal.
For example, consider the thread-safety aspect of implementing a Singleton object. If the library fully
encapsulates threading, then the user of a particular, nonportable threading system cannot use the Singleton
library. If the library gives access to the unprotected primitive functions, there is the risk that the
programmer will break the design by writing code that's syntactically—but not semantically—valid.
What if the library implements different design choices as different, smaller classes? Each class would
represent a specific canned design solution. In the smart pointer case, for example, you would expect a
battery of implementations:
SingleThreadedSmartPtr, MultiThreadedSmartPtr,
RefCountedSmartPtr, RefLinkedSmartPtr, and so on.
The problem that emerges with this second approach is the combinatorial explosion of the various design
choices. The four classes just mentioned lead necessarily to combinations such as
SingleThreadedRefCountedSmartPtr. Adding a third design option such as conversion support
leads to exponentially more combinations, which will eventually overwhelm both the implementer and the
user of the library. Clearly this is not the way to go. Never use brute force in fighting an exponential.
Not only does such a library incur an immense intellectual overhead, but it also is extremely rigid. The
slightest unpredicted customization—such as trying to initialize default-constructed smart pointers with a
particular value—renders all the carefully crafted library classes useless.