CSC/ECE 517 Fall 2009/wiki3 9 rp
The Non-Redundancy Principle
In Bertrand Meyer's Object Oriented Software Construction, he outlines many principles to increase reusability by managing complexity in software design. Some of these apply primarily to object oriented programming, primarily the principles dedicated to abstract data types. Most of the principles, however, apply to programming generally. The first five discussed in the book are often discussed in articles (the "Open-Close Principle" perhaps being the most popular), but equally important principles are discussed later in the book as well. One such principle, presented in the section on Contracting For Software Reliability, is called The Non-Redundancy Principle.
Definition
As with all principles presented in Object Oriented Software Construction, the principle itself is quite terse:
Under no circumstances shall the body of a routine ever test for the routine's precondition.
As simple as this may seem on first blush, it has extensive implications.
Before we discuss the implications, it is proper to provide a bit of context regarding preconditions, postconditions and programming by contract.
Context
Meyer outlined the notion of programming by contract, which boils down to a methodology to enforce software modularity by clearly defining the relationships between various components, much in the same way relationships between individuals and/or companies are formalized in contracts.
In Chapter 11, Meyer indicates that for each party in the contract, the aspects of the contract include:
- Rights
- Obligations
- Preconditions
- Postconditions
In short, these indicate what is expected of the supplier and the client (terms describing the roles of objects that are party to the contract). Specifically, a precondition is a minimum criteria that must be satisfied on the part of the client for the contract to be valid. The heart of the Non-Redundancy Principle is who checks that the preconditions are satisfied, and when are those checks made.
It must be decided whether the client or the supplier of a service should enforce the preconditions. Once that is made clear, the software designer must decide when the precondition will be enforced, and once it is enforced, it should never be checked.
Meyer goes to some length to justify this position, which flies in the face of the defensive programming mantra, which generally encourages as much checking as possible, ostensibly to help eliminate errors "just in case". As we will see in the following sections, and as Meyer describes, it is difficult to justify this approach, purly on the grounds that such redundant checking inordinately increases the complexity of the software.
Implications
There are a variety of implications that emerge from the Non-Redundancy Principle.
DRY Principle
A common mantra in programming is "Don't Repeat Yourself" or simply "DRY" for short. In it's simplest form, the Non-Redundancy Principle is a specialization of DRY, in that it prohibits developers from repeating themselves.
Consider the example that Meyer uses of a function responsible for returning a real positive square root of a number. In Java, such a function might look like:
/** * Returns the postive real square root of the input, d. */ public double sqrt(double d) { return Math.sqrt(d); }
What happens if the number passed in is negative? Will the method return a real square root? We have specified in the supplier (in this case, the method called "sqrt") is obligated to return a positive real square root. By specifying a precondition that the input must be greater than or equal to zero, we now also qualify that obligation by imposing it only if the precondition is satisfied.
/** * Returns the postive real square root of the input, d. * Precondition: input d >= 0 */ public double sqrt(double d) { return Math.sqrt(d); }
Now we have a precondition. But what does all this have to do with the Non-Redundancy Principle?
It might be tempting to then write this:
/** * Returns the postive real square root of the input, d. * Precondition: input d >= 0 */ public double sqrt(double d) { if (d < 0) throw new IllegalArgumentException("Input must be greater than or equal to zero."); return Math.sqrt(d); }
But, according to the Non-Redundancy Principle, this is prohibited! Why? Well, what does Math.sqrt do? Does it also check for a value less than zero? It turns out that it does. But the point here is that if we use the code directly above this paragraph, our system now checkes for numbers less than zero twice. Not only have we repeated ourselves, but we have increased the complexity of the code, which brings us to how the Non-Redundancy Principle reduces code complexity.
Code Complexity
One way to measure code complexity is through cyclomatic complexity (sometimes called the McCabe Number). Simply put, cyclomatic complexity measures the number of different paths through a piece of code. It is often cited as a good measure of how maintainable a piece of code is, and certainly has an impact on how burdensome testing will be. In the example above:
public double sqrt(double d) { if (d < 0) throw new IllegalArgumentException("Input must be greater than or equal to zero."); return Math.sqrt(d); }
We can immediately see that adding the check for the precondition to the method added an additional exit from the method, namely, the method can now throw an IllegalArgumentExecetion. Taken singly, this may not seem like a big deal, but imagine methods with two or more preconditions, and imagine every method in the system starts checking for each. On a systemic scale, redundant checking vastly increases the code bulk, complexity, and testing burden of the system.
Run-time versus Compile-time Errors
It may be tempting to look at systems that enforce preconditions to be in violation of the Non-Redundancy Principle. After all, in the way that Meyer describes design by contract, if the precondition for a method is violated, its behavior is undefined. But since Object Oriented Software Construction was written, new techniques have emerged to ensure that such preconditions are not violated, but they do so without checking for the precondition. Rather, they are codified statements of the precondition.
One such example can be seen in JSR 308 for Java. It specifies that Java annotations could be applied to types (in addition to classes and methods). Indeed, the code to support this has ben written, and is available in the form of the Checker Framework.
This type of annotation allows a developer to specify, using type annotations, that a certain precondition must be met. For example, we might want to specify that a particular reference should never be null, perhaps because we want to call a method on it:
/** * Precondition: ref must be non-null. */ public Class(Object ref) { ref.getClass(); }
In this case, if the precondition is violated, we get a RunTimeError (NullPointerException). If we instead using the checker framework, we can say the same thing, but in a way the compiler (and developers!) can understand:
public Class(@NonNull Object ref) { ref.getClass(); }
In this case, if a value that might be null is passed in, the compiler will produce an error. Thus, this approach moves runtime errors to compile time. At first blush it may appear that the checker framework is "checking" for the precondition, but note that it doesn't carry any of the hallmarks Meyer discusses: it does not impact performance, it does not increase cyclomatic complexity, and it does not make the code any bulkier.
More and more modern languages this support of framework. For example, Scala supports it through a pluggable compiler architecture. It is worth noting that such systems, rather than violating the Non-Redundancy Principle, in fact not only comply, but go one step further by, in many cases, eliminating the need for checks at all!
Glossary
- Client
- A class that uses the features of another, its supplier, on the basis of the supplier's interface specification (contract)
- Contract
- The set of precies conditions that govern the relations between a supplier class and its clients. The contract for a class includes the individual contracts for the exported routines of the class, represented by preconditions and postconditions, and the global class properties, represented by the class invariant.
- Design By Contract
- A method of software construction that designs the components of the system so that they will cooperate on the basis of precisely defined contracts.
- Postcondition
- An assertion attached to a routine, which must be guaranteed by the routine's body on a return from any call to the routine if the precondition was satisfied on entry. Part of the contract governing the routine.
- Precondition
- An assertion attached to a routine, which must be guaranteed by every client prior to any call to the routine. Part of the contract governing the routine.
- Supplier
- A class that provides another, its client, with features to be used through an interface specification (contract).