Thursday, January 6, 2011

Don't Tolerate Tolerant Software


Consider the following code:

void Panel::AddWidget(Widget* widget)
{
  if (widget != NULL)
    widgets_.push_back(widget)
}
Figure 1

I kept running into functions following a similar pattern. The entire body of a function would be wrapped in an if statement, checking for one or more validity conditions such as non-null arguments. This pattern is problematic because it is misleading to the client of the function; because the above function is called AddWidget, one expects it to add a widget to the panel. However, this will not be the case when NULL is passed in as the widget argument.

The problem is that there is no sensible way for AddWidget to handle NULL. NULL shouldn't have been passed into this method in the first place. If a function can only handle calls for which the arguments meet certain validity conditions, those conditions should be asserted at the top of the function. A better version of AddWidget might look like this:1

void Panel::AddWidget(Widget* widget)
{
  assert(widget != NULL);
  widgets_.push_back(widget)
}
Figure 2

The function in figure one is called tolerant. A tolerant function is one which tries to handle all argument values conforming to its type signature, even if its intended domain is a subset of those values. Tolerant functions should be avoided; instead, functions should be demanding, like the one shown in figure 2. A demanding function is one which asserts that its arguments perfectly conform to its expectations, and whose body is written as if those expectations have been met.

We can generalize the concept of tolerance to a finer level of granularity than that of functions. In general, code is considered tolerant if it tries to handle situations that can only occur as the result of programming errors. No line of code should be tolerant; for example, consider the situation in which a caller function obtains a value which is returned from a callee function. The comment above the declaration of the callee should unambiguously describe the value it returns, and the caller should not try to handle return values which do not conform to this description.

One might object to my claim that demanding code is always superior to tolerant code on the grounds that some unexpected conditions need to be handled. A program interpreting a user-produced xml file, for example, would need to handle cases in which the xml file is not well formed. This is simply a misinterpretation of the definition of tolerant code; tolerant is being mistaken for robust.

Code is robust if it handles every situation, however obscure, which could occur during execution assuming the absence of programming errors. Since a programmer can do nothing to prevent incorrectly formatted user input, a program which handles incorrectly formatted input is considered robust, not tolerant. Obviously, robustness is a desirable property, unlike tolerance.

Programmers who write tolerant code argue that it results in a shipped product which is less likely crash. This isn't clear to me, because handling unexpected conditions often results in code which is vastly more complex than the equivalent demanding would be. This added complexity introduces opportunities for new, unexpected conditions. These unexpected conditions are often not handled. Even if such conditions were handled, that would obscure the expected execution path to the point of absurdity.

Some programmers write code that is both demanding and tolerant-- they assert expected conditions, but then proceed to handle the cases in which the conditions are false. The intention of this style of programming is to alert developers of programming errors whenever they are detected, but still fail gracefully if such errors make it into release builds. Here is an example of some written using this “both worlds” approach:

  1. assert(thisShouldNeverHappen)
  2. if (thisShouldNeverHappen)
  3. {
  4.   //error handling code goes here
  5. }
I don't think this approach works. Its most glaring problem is that the code inside the if block will never get tested; if the assert fails during the debugging process, the program is typically halted, and a programmer is supposed to correct the code so that the assert can no longer fail.3

Tolerant code is the single largest detractor from software quality that I have faced. Every programmer should strive to write demanding code, yet many don't, some even believing that it is irresponsible or undesirable. I would like this state of affairs to change, so I leave you with a plea:

Software engineering professors, enthusiastically espouse demanding on multiple occasions. Find a way to ensure that every student who attends your classes understands the advantages of demanding code.

Authors of programming books, emphasize a demanding coding style. Beginning programmers should learn the virtues of demanding code around the same time they learn about for loops and functions.

Professional software engineers, be a stickler for demanding code when performing code reviews.

Hobbyists, think about the implications of adopting various coding styles. Search for a highly readable, maintainable, and demanding style of programming.



Credits:
This post was influenced by a discussion on lambda the ultimate. Thanks to all who participated.

Footnotes:
1: Of course, another possible way to rule out invalid arguments is to give them more specific types. If one's coding standard allows for it, changing the widget argument from a pointer to a reference is a viable option.

2: This terminology was taken from chapter 11 of Bertrand Meyer's excellent book Object-Oriented Software Construction.

3: According to Code Complete, this “both worlds” approach is used by the Microsoft Word team as policy! I find this somewhat befuddling, but suspect I don't have the whole story.