turns its value. How should the function behave if the variable is not set?
There are several options:
˲ Throw a VariableNotSet exception.
˲ Return null.
˲ Return the empty string.
Throwing an exception is appropriate if the designer anticipates that
looking for a variable that isn’t there
is not a common case and likely to
indicate something that the caller
would treat as an error. If so, throwing
an exception is exactly the right thing
because exceptions force the caller to
deal with the error. On the other hand,
the caller may look up a variable and, if
it is not set, substitute a default value.
If so, throwing an exception is exactly
the wrong thing because handling an
exception breaks the normal flow of
control and is more difficult than testing for a null or empty return value.
Assuming that we decide not to
throw an exception if a variable is not
set, two obvious choices indicate that a
lookup failed: return null or the empty
string. Which one is correct? Again,
the answer depends on the anticipated use cases. Returning null allows the
caller to distinguish a variable that is
not set at all from a variable that is set
to the empty string, whereas returning the empty string for variables that
are not set makes it impossible to distinguish a variable that was never set
from a variable that was explicitly set
to the empty string. Returning null is
necessary if it is deemed important to
be able to make this distinction; but,
if the distinction is not important, it is
better to return the empty string and
never return null.
General-purpose APIs should be “
pol-icy-free;” special-purpose APIs should be
“policy-rich.” In the preceding guideline, I mentioned that correct design
of an API depends on its context. This
leads to a more fundamental design
issue—namely, that APIs inevitably
dictate policy: an API performs optimally only if the caller’s use of the API
is in agreement with the designer’s
anticipated use cases. Conversely, the
designer of an API cannot help but
dictate to the caller a particular set
of semantics and a particular style of
programming. It is important for designers to be aware of this: the extent
to which an API sets policy has profound influence on its usability.
If little is known about the context
in which an API is going to be used, the
designer has little choice but to keep
all options open and allow the API to
be as widely applicable as possible. In
the preceding lookup example, this
calls for returning null for variables
that are not set, because that choice
allows the caller to layer its own policy
on top of the API; with a few extra lines
of code, the caller can treat lookup of
a nonexistent variable as a hard error, substitute a default value, or treat
unset and empty variables as equivalent. This generality, however, comes
at a price for those callers who do not
need the flexibility because it makes it
harder for the caller to treat lookup of
a nonexistent variable as an error.
This design tension is present in
almost every API—the line between
what should and should not be an error is very fine, and placing the line
incorrectly quickly causes major pain.
The more that is known about the context of an API, the more “fascist” the
API can become—that is, the more
policy it can set. Doing so is doing a
favor to the caller because it catches
errors that otherwise would go undetected. With careful design of types
and parameters, errors can often be
caught at compile time instead of being delayed until run time. Making the
effort to do this is worthwhile because
every error caught at compile time is
one less bug that can incur extra cost
during testing or in the field.
The Select() API fails this guideline because, by overwriting its arguments, it sets a policy that is in direct
conflict with the most common use
case. Similarly, the .NET Receive()
API commits this crime for nonblocking sockets: it throws an exception if
the call worked but no data is ready,
and it returns zero without an exception if the connection is lost. This is
the precise opposite of what the caller
needs, and it is sobering to look at the
mess of control flow this causes for
the caller.
Sometimes, the design tension
cannot be resolved despite the best efforts of the designer. This is often the
case when little can be known about
context because an API is low-level
or must, by its nature, work in many
different contexts (as is the case for
general-purpose collection classes,
for example). In this case, the strategy pattern can often be used to good
effect. It allows the caller to supply
a policy (for example, in the form of
a caller-provided comparison function that is used to maintain ordered
collections) and so keeps the design
open. Depending on the programming
language, caller-provided policies can
be implemented with callbacks, virtual functions, delegates, or template
parameters (among others). If the API
provides sensible defaults, such externalized policies can lead to more flexibility without compromising usability
and clarity. (Be careful, though, not to
“pass the buck,” as described later in
this article.)
APIs should be designed from the perspective of the caller. When a programmer is given the job of creating an
API, he or she is usually immediately
in problem-solving mode: What data
structures and algorithms do I need
for the job, and what input and output parameters are necessary to get
it done? It’s all downhill from there:
the implementer is focused on solving
the problem, and the concerns of the
caller are quickly forgotten. Here is a
typical example of this:
makeTV(false, true);
This evidently is a function call that
creates a TV. But what is the meaning
of the parameters? Compare with the
following:
makeTV(Color, FlatScreen);
The second version is much more
readable to the caller: even without
reading the manual, it is obvious that
the call creates a color flat-screen TV.
To the implementer, however, the first
version is just as usable:
void makeTV(
bool isBlackAndWhite,
bool isFlatScreen)
{ /* ... */ }
The implementer gets nicely named
variables that indicate whether the TV
is black and white or color, and whether it has a flat screen or a conventional
one, but that information is lost to the
caller. The second version requires
the implementer to do more work—