that are ready or zero, in which case
the call returned because the time-out
was reached. With this simple change,
the usability problems disappear and,
because the caller no longer needs to
copy the arguments, the code is far
more efficient as well.
There are many other ways to fix the
problems with Select() (such as the
approach used by epoll()). The point
of this example is not to come up with
the ultimate version of Select(), but
to demonstrate how a small number
of minor oversights can quickly add
up to create code that is messy, difficult to maintain, error prone, and
inefficient. With a slightly better interface to Select(), none of the code I
outlined here would be necessary, and
I (and probably many other programmers) would have saved considerable
time and effort.
figure 2: the doselect() function.
public void doSelect(List checkRead, List checkWrite,
List checkError, int milliseconds)
{
the cost of Poor aPis
The consequences of poor API design
are numerous and serious. Poor APIs
are difficult to program with and often
require additional code to be written,
as in the preceding example. If nothing else, this additional code makes
programs larger and less efficient because each line of unnecessary code
increases working set size and reduces CPU cache hits. Moreover, as in the
preceding example, poor design can
lead to inherently inefficient code by
forcing unnecessary data copies. (
Another popular design flaw—namely,
throwing exceptions for expected
outcomes—also causes inefficiencies
because catching and handling exceptions is almost always slower than
testing a return value.)
The effects of poor APIs, however,
go far beyond inefficient code: poor
APIs are harder to understand and
more difficult to work with than good
ones. In other words, programmers
take longer to write code against poor
APIs than against good ones, so poor
APIs directly lead to increased development cost. Poor APIs often require not
only extra code, but also more complex
code that provides more places where
bugs can hide. The cost is increased
testing effort and increased likelihood
for bugs to go undetected until the
software is deployed in the field, when
the cost of fixing bugs is highest.
Much of software development
ArrayList readCopy; // Copies of the three parameters because
ArrayList writeCopy; // Select() clobbers them.
ArrayList errorCopy;
if (milliseconds <= 0) {
// Simulate waiting forever.
do {
// Make copy of the three lists here...
Select(readCopy, writeCopy, errorCopy, Int32.MaxValue);
} while ((readCopy == null || readCopy.Count == 0) &&
(writeCopy == null || writeCopy.Count == 0) &&
(errorCopy == null || errorCopy.Count == 0));
} else {
// Deal with non-infinite timeouts.
while ((milliseconds > Int32.MaxValue / 1000) &&
(readCopy == null || readCopy.Count == 0) &&
(writeCopy == null || writeCopy.Count == 0) &&
(errorCopy == null || errorCopy.Count == 0)) {
// Make a copy of the three lists here...
Select(readCopy, writeCopy, errorCopy,
( Int32.MaxValue / 1000) 1000);
milliseconds -= Int32.MaxValue / 1000;
}
}
if ((readCopy == null || readCopy.Count == 0) &&
(writeCopy == null || writeCopy.Count == 0) &&
(errorCopy == null || errorCopy == 0)) {
Select(checkRead, checkWrite, checkError, milliseconds*1000);
}
// Copy the three lists back into the original parameters here...
}
is about creating abstractions, and
APIs are the visible interfaces to these
abstractions. Abstractions reduce
complexity because they throw away
irrelevant detail and retain only the
information that is necessary for a
particular job. Abstractions do not
exist in isolation; rather, we layer abstractions on top of each other. Application code calls higher-level libraries
that, in turn, are often implemented
by calling on the services provided by
lower-level libraries that, in turn, call
on the services provided by the system
call interface of an operating system.
This hierarchy of abstraction layers
is an immensely powerful and useful
concept. Without it, software as we
know it could not exist because programmers would be completely overwhelmed by complexity.
The lower in the abstraction hierarchy an API defect occurs, the more
serious are the consequences. If I mis-design a function in my own code, the
only person affected is me, because
I am the only caller of the function. If
I mis-design a function in one of our
project libraries, potentially all of my
colleagues suffer. If I mis-design a
function in a widely published library,
potentially tens of thousands of programmers suffer.
Of course, end users also suffer. The
suffering can take many forms, but the
cumulative cost is invariably high. For
example, if Microsoft Word contains a
bug that causes it to crash occasionally
because of a mis-designed API, thousands or hundreds of thousands of
end users lose valuable time. Similarly,
consider the numerous security holes
in countless applications and system
software that, ultimately, are caused
by unsafe I/O and string manipulation
functions in the standard C library
(such as scanf() and strcpy()). The
effects of these poorly designed APIs
are still with us more than 30 years
after they were created, and the cumulative cost of the design defects easily
runs to many billions of dollars.
Perversely, layering of abstractions
is often used to trivialize the impact
of a bad API: “It doesn’t matter—we
can just write a wrapper to hide the
problems.” This argument could not
be more wrong because it ignores the