Site Archive (Complete)
C++
SELECTIVE IGNORANCE

Finding the Signal in the Noise

by Andrew Koenig

January 2008


January 21, 2008

When code and documentation disagree


When you find that your code's behavior doesn't match the documentation, which one do you change?

I think that most people starting out in programming would change the documentation. The basis for this viewpoint is that the documentation's purpose is to describe the program's behavior so that the programmer can figure out what to expect. If there is a discrepancy, it means that the documentation is wrong; the easiest way to resolve that discrepancy is to fix the error in the documentation.

With experience comes a more sophisticated viewpoint: The documentation is the system designer's contract with the user, and one cannot change that contract unilaterally. The developer's job is to produce software that conforms to that contract; if it fails to do so, it is the software, not the contract, that must change.

A simple example should serve to reinforce this second viewpoint: If a program adds 2 and 2 and gets 5, the way to fix it is not to change the documentation to say that 2 + 2 = 5.

Still further reflection should reveal still a more sophisticated view: Sometimes documentation will contain mistakes, just as software contains mistakes. So when there is a discrepancy, the wisest course of action is to figure out whether the program or its documentation that is in error, and then to fix the one that is broken.

If only reality were that simple. Once upon a time I was working on a project in which we found a discrepancy between code and documentation: A library routine took two parameters, and the documentation described those parameters in the opposite order from the code. If you used this library in the way that the documentation described, your program would surely fail.

This situation gave us a choice of changing the code or the documentation. The argument that eventually won was the one about the documentation being a contract with the user: Users would probably have copies on their shelves of the documentation we had already published, and those copies would persist long afer the code had been superseded. So we corrected the next release of the library to make it match the documentation.

Of course, this decision was a disaster, because it effectively renedered that library routine useless. The problem was that it became impossible to write code that would work with both versions of the library, which meant that every piece of code that used that library routine had now to be written in two versions: one for the old library and one for the new one. Still worse: There was no reliable way of testing which library was in use, so programmers had to select the right version of the code manually.

This situation came about because of several factors.

First, it was impossible to use the program in the way the documentation said it should work. Thus, anyone who wound up using the program successfully did so by violating the documentation.

Second, "fixing" the code changed its previously useful (albeit undocumented) behavior in an incompatible way. So the programmers who had discovered how to circumvent the documentation error found that their programs stopped working.

Finally, there was no way to unify the old and new behavior. That is, there was no way to write a single program that would work with both versions of the library.

In hindsight, I think that perhaps the best way to resolve this problem would have been to have change the documentation and find a way to warn programmers that the original documentation was incorrect. After all, there was no way to follow the original documentation and obtain a working program.

Posted by Andrew Koenig at 11:45 AM  Permalink |


January 02, 2008

The trouble with testing


There is a school of thought that says that when you set out to write a program, you should write the tests first. Your goal is then to write the simplest program that passes the tests. Once you have done so, you're done.

This approach is certainly appealing. In particular, having automated tests that capture as much as possible of a program's desired behavior is an excellent idea. But what if a program has to have a characteristic that you don't know how to test?

One possible answer to this question is that there are no such characteristics -- if you can't figure out how to write a test for a program's behavior, you have no way of observing the behavior, and therefore you should not care whether the program behaves that way.

Although that point of view is appealing, I can think of several counterexamples.

The first is that even a simple program may have so many possible inputs that there is no way to test them all, and it might not even be possible to think of a set of representative samples. As an example, consider a program that does a floating-point multiplication. Suppose you are trying to verify that the program conforms to the relevant IEEE floating-point standard. There isn't enough time in the world for you to test every possible pair of input values, so you have to select them somehow. But the moment you do so, you run into the possibility that a bug may lurk in one of the possibilities you didn't select.

The second counterexample might come from a requirement that a program not leak memory. It's hard to express such a requirement in terms of specific numbers, because the requirement really governs asymptotic behavior. In such circumstances, it is typically a judgment call as to whether the requirement has been met in a particular case.

The third counterexample is related: It is often important that a program be robust against incorrect input. In particular, programs, such as web applications, that take input from the public have to be robust against malicious attempts to manipulate them with inappropriate input. In such cases, the cost of forgetting to test for a particular hazard may be the loss of the entire system of which the program is a part. Worse, that hazard might be part of a library component that there is no easy way to test directly.

What can we do in such circumstances? The best approach I can think of is to try to verify the program's behavior independently of testing it. Here I am using the word "verify" to mean reasoning about the program with an eye toward showing that certain kinds of failures are impossible. If, for example, you can locate every place in a program where memory is allocated and prove that the memory is always freed, then you have just increased your confidence that the system as a whole doesn't leak memory. One way of proving such behavior might be to show that memory is allocated only in constructors and is always freed in the corresponding destructors. In that case, the program can only leak memory if it leaks objects, and it may well be much easier to prove that that cannot happen.

I don't want to minimize the importance of testing. Indeed, I have some interesting examples that show just how important testing can be. But even when a program has passed all its tests, I think it's a mistake to assume that it's incapable of improvement.

Posted by Andrew Koenig at 10:06 AM  Permalink |



February 2008
Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29  


BLOGROLL
 
INFO-LINK


Related Sites: DotNetJunkies, SD Expo, SqlJunkies