[Update, 3/28/07: Changed recommendation to not use conditional compilation constants to vary behavior between debug and release mode.]
There are two things I loathe during development: spending any time at all with the VS.NET debugger and seeing "object reference not set to an instance of an object" (which is usually why I'm in the debugger to begin with). The technique of Design-by-Contract (DBC) allows you to not only eschew these two issues but also assists with writing better self-documenting and more expressive code. Design-by-Contract isn't a new technique; in fact, the language Eiffel has DBC built right into it. There's a lot we can learn (and take) from Eiffel's approach.
Implicit Contracts
Typically, if you're not using DBC techniques, comments are (hopefully) included to describe to the client how the code should be used and any caveats to be expected. (The "client" could be a third party calling your API, another developer on your own team, or, most often the case, another bit of code you're writing yourself.) As an example of an implicit contract - read "not DBC" - the following method fully describes the "contractual obligations" of the calling code:
/// <summary>Gets the user for the given ID.</summary>
/// <param name="id">Should be > 0</param>
public User GetUserWithIdOf(int id,
UserRepository userRepository) {
return userRepository.GetById(id);
}
The developer of this code has documented the restrictions for using the method - the "calling contract" - and, implicitly, has promised to return a User. There are a few problems with this:
- If the client didn't pay attention to the comments, there's nothing to stop the client from passing in an invalid ID of 0.
- The client could easily pass null for userRepository. This would result in an "object reference" exception.
- Since the returned User may be null, the client will have to check for this possibility or also risk an "object reference" exception. In other words, the target method has not obligated itself to any sort of "response contract."
- What if, down the road, it becomes OK to pass 0 to the method. If the developer neglects to update the comment, then the stated business rule will conflict with the inherit business rule within the code itself.
Explicit Contracts
Design by Contract allows you to transform the implicit contract, described above, into an explicit bidirectional contract. The bidirectional contract obligates both the client to invoke the method in a particular way and for the target method to guarantee a particular result. If the contract is broken, then the breaking party will be severely chastised (conditionally...more on this soon). The calling contract is expressed with one or more "pre-conditions" while the response contract is expressed with one or more "post-conditions." A pre-condition states "this is your end of the bargain which must be adhered to to call me." A post-condition states "this is my end of the bargain which you can count on me to enforce." What follows is a very explicit, and very inflexible, bidirectional contract:
/// <summary>Gets the user for the given ID.</summary>
public User GetUserWithIdOf(int id,
UserRepository userRepository) {
// Pre-conditions
if (userRepository == null)
throw new ArgumentNullException(
"userRepository");
if (id <= 0)
throw new ArgumentOutOfRangeException(
"id must be > 0");
User foundUser = userRepository.GetById(id);
// Post-conditions
if (foundUser == null)
throw new KeyNotFoundException("No user with " +
"an ID of " + id.ToString() +
" could be located.");
return foundUser;
}
With the bidirectional contract now in place, almost all of the previous drawbacks have been addressed. Furthermore, the code is much more self-documenting and requires very few comments to be explicit. But a few drawbacks still exist:
- The contract is a bit verbose and easily blends in with the surrounding code. Preferably, the contract should be more concise and easily spotted.
- The contract above always throws exceptions. It should be simpler to conditionally turn on exceptions for debug vs. release mode. Furthermore, it should be simpler to always throw exceptions for pre-conditions, but only throw exceptions for post-conditions in debug mode.
- What if we want to extend contracts to the class level, and not just on individual methods? The above technique is difficult to extend for these purposes.
- What if we want class level contracts to be extended (or restricted) within inherited classes? Again, the above technique would become cumbersome - read "fragile and difficult to maintain" - when inheritance is involved.
Design-by-Contract Class Library
Fortunately, a light-weight class library has been written for the .NET environment which addresses these issues: http://www.codeproject.com/csharp/designbycontract.asp [1]. With this library in place, the previous example could be expressed as follows:
/// <summary>Gets the user for the given ID.</summary>
public User GetUserWithIdOf(int id,
UserRepository userRepository) {
Check.Require(userRepository != null,
"userRepository may not be null");
Check.Require(id > 0, "id must be > 0");
User foundUser = userRepository.GetById(id);
Check.Ensure(foundUser != null, "No user with an " +
"ID of " + id.ToString() + " could be located.");
return foundUser;
}
Now, the contract is concise, explicit and easy to spot. The library also provides for inheritance, allowing you to (only) weaken preconditions and (only) strengthen postconditions in overriding methods. (As an exercise for the reader, you'll also want to explore this library's inclusion of an "invariant" which allows contracts to be applied to the class level.) Finally, a few conditional compilation constants are provided to vary behavior between debug and release mode. I recommend not varying the conditional compilation constants regardless of the compilation mode. As commenters have noted, and as I have seen from using the library, behavior should not vary between debug in release mode. A contract is always a contract. Furthermore, variations in behavior may lead to difficult-to-reproduce bugs. In my own, modified version of the DBC library, I have removed all capabilities of varying the contractual behavior based on compilation mode and, therefore, have all conditions turned on at all times.
Day-to-Day Use & Benefits
From a practical standpoint, once you have the DBC class library in place, you'll find the "Check.Require" to be, by far, the most useful addition to your coding arsenal. In my projects, I find myself having one or two of these at the top of every method. They only take a few seconds to write and save many hours in the debugger. In fact I haven't used, or needed, the debugger since adopting the principles of design-by-contract in earnest. The benefits of Design-by-Contract [2] include:
- A better understanding of the method and how the software is constructed.
- A systematic approach to building bug-free systems.
- An effective framework for debugging, testing, and more generally, quality assurance.
- A method for self-documenting software components.
I hope this give a hint behind the benefits of design-by-contract...they'll certainly become apparent the very first day you include the DBC class library in your own project.
References and Additional Resources
Posted
09-22-2006 9:51 AM
by
Billy McCafferty