(There is a summary of this article, in three short bullet points, at the end.)
The concept of defensive programming is misunderstood by many people, which is a shame, because it is an elementary and basic notion.
I was shocked to recently hear some graduate students in programming languages state that it's a matter of opinion whether a particular run-time check is defensive or not. Some said (incorrectly) that handling expected error conditions, such as a web server being down, qualifies as defensive programming.
Understanding defensive programming requires understanding the concept of a specification. A specification indicates the legal inputs to a component, and indicates the required behavior/output.
If an input is legal according to the specification, then an implementation must adhere to the specification. Multiple implementation behaviors may be possible. Consider a procedure that returns an approximation to the square root of its argument, accurate to within .001: for each input, many different outputs are acceptable. Likewise, an iterator over the contents of a set may return the elements in an arbitrary order.
If an input is illegal according to the the specification, then an implementation is permitted to do anything at all. The implementation may return a nonsensical output, or crash, or delete all your files.
The procedure's caller has no recourse to complain about this behavior: the specification serves as a contract in which the caller is required to satisfy the input requirements and the callee is required to satisfy the output requirements. If one party to a contract (the caller) breaks the contract, then the other party (the callee) is no longer bound by it.
When the input is illegal, the implementer of the procedure is free to do whatever is convenient. However, the implementer may choose, out of the goodness of his/her heart, to go to extra effort to give a useful result (such as a useful error message) even when the client has violated the contract. This is defensive programming. The client may find this useful, but can't depend on it. The implementer may drop the extra effort at any time, and the implementation remains in conformance to the specification.
If it's important to the caller that — even when the caller has screwed up — the procedure behaves in a particular way, then the caller should choose a library with a different specification that gives that guarantee. Such a specification would dictate behavior for all possible inputs instead of just specifying behavior for a subset of possible inputs.
Why don't all specifications dictate behavior for all possible inputs? It may be inconvenient, or it may be inefficient, to do so. Consider the example of binary search, which takes as an input a sorted array and a value, and reports whether the value appears in the array. If the input is not sorted, the implementation is allowed to do anything, including reporting that a given value is not present (even though it does appear in the (unsorted) array), or crashing. A sloppy client that sometimes incorrectly passes a non-sorted array to the binary search routine might desire to get an exception indicating the mistake, rather than other behaviors. In this case, the client should either do the checking itself, or choose a different library whose specification guarantees throwing the exception. Such a library will be much less efficient than one that does not check whether the array is sorted! Even simple checks of inputs can impact performance, code size, and readability, and it is reasonable that implementers choose not to always implement these optional defensive checks.
An interesting property is that you can't tell whether a given check is defensive programming just based on the code. You have to examine the specification.
What if a given piece of code has no (written-down) specification? In that case, it's rather odd to even argue about whether it is correct. Asking whether the code is correct is like asking whether "42" is the correct answer to a question, without specifying what the question is. If you have code that lacks an unambiguous written specification, then it makes no sense to point fingers in deciding who is at fault, because that question is subjective. I assure you that no matter how "obvious" you think the intended specification is, reasonable people can and will interpret it differently. So, in the absence of a specification, first write one. You will reap generous rewards in better understanding of how your system works.
I should note that the benefits of writing a specification are quite distinct from the benefits of defensive programming. You should always use the former, and use the latter whenever appropriate.
In summary:
- Defensive programming changes the behavior of a module in the face of client errors, when a client does something that is explicitly forbidden by the module's contract.
- Whether a given check is defensive programming depends on the module's specification. If the specification mentions given behavior, then that behavior is never defensive programming — it is just programming.
- If a client depends on certain behavior, then the client should choose a module whose specification guarantees that behavior, rather than hoping that the module happens to perform defensive checks that the client needs.
1 comment:
The worst kind of defensive programming I've seen is when an implementation detects incorrect (as per the spec) argument but, instead of failing (possibly with an exception or an error), it pretends that nothing happened. This usually results in an error further down the line, which is very hard to trace back to the original condition.
Post a Comment