Design by Contract: An Alternative to Testing for Code Quality

By Eric — 7 minute read

I recently finished reading Design by Contract by Example by Richard Mitchell and Jim McKim. The book, as its title suggests, gives advice about writing good contracts through a set of principles applied in various examples. In addition to wanting to learn more about design by contract generally, I came to this book with a couple questions: Can you do design by contract effectively without built-in language support, and what is the relationship between design by contract and unit testing? In this article I'll talk about design by contract generally, and discuss those two questions in follow-on articles.

Definition

Design by contract (DBC) is a methodology for specifying precisely how an object is supposed to behave. The contract relationship is between an object and the user (or client) of the object (probably some other object), and each party has its obligations as part of the agreement. Like a typical contract in the legal sense, one party says to the other, "Given that you do x, I agree to do y."

Contracts in DBC are specified as assertions. Some assertions, called preconditions, assert that the user of the object (or client) is fulfilling its part of the agreement. Others, called postconditions and invariants, assert that the object is fulfilling its part of the contract. The assertions are executable parts of the program, so running the program ensures that the contract is being fulfilled by both parties, and that the program is therefore correct. Any violation of the contract produces an easy-to-notice error at runtime.

Preconditions and postconditions apply to methods, while invariants apply to whole objects. A precondition is a prerequisite for calling a method, and a postcondition is what is guaranteed to be true when the method finishes. An invariant is something that, from the client's perspective, is always true about an object.

The Book

Mitchell and McKim give advice how to effectively write contracts through a set of principles and guidelines. For example, their first guideline is to divide object methods into two categories: queries and commands. Queries return some value, but don't effect the visible state of the object (like a const method in C++). Commands, on the other hand, don't return a value but may change the object's state. In other words, queries don't have side effects but commands do. This division makes sense because when writing assertions, you call the queries, and it would be bad if checking contracts changed the state of the object. The authors further distinguish basic queries and derived queries. Derived queries are expressible in terms of one or more basic queries, and might exist for convenience or performance reasons (more on that in a bit).

Most of the eponymous examples in the book are written in Eiffel, the programming language developed by Bertrand Meyer, the originator of design by contract (and author of this book's Foreword). Even if you don't know Eiffel, though, the examples are pretty easy to follow. There is a chapter that uses Java, which I would recommend reading out of order if that language is more comfortable to you, because comparing the Java and Eiffel examples clarify some things about Eiffel that the authors don't explicitly mention.

Here's an example of a contract for a generic "add to a list" method using Eiffel syntax:

add(new_item : ITEM)
  -- Adds an item to the list
  require
    item_not_already_added:
      not has_item(new_item)

  ensure
    item_count_increased:
      count = old count + 1
    item_added:
       has_item(new_item)

An assertion uses the require (precondition) or ensure (postcondition) keywords (or invariant, which isn't shown here). Then there is a label that can be used to state the assertion in logical terms. The label is displayed as part of the error message if an assertion fails. After the label is a boolean expression that should evaluate to true if all is working as expected. There are two queries that are also part of the class, has_item and count, which are used to write the contracts. Take special note of the old keyword. This lets you refer to the value of something as it was before the method started executing so that you can write postconditions in terms of how they change the state from what it was originally.

When this method is called at runtime, the precondition is first evaluated to verify that the caller is living up to its part of the contract. Assuming that passes, the body of the method is executed. Then, the postconditions are evaluated. If there were an invariant assertion for the class (like that count can't be negative), that would also be evaluated. An invariant is shorthand for saying that all the methods in the class have a particular postcondition. The postconditions and invariants verify that the object is fulfilling its part of the contract.

The book gives examples of contracts for some common data structures, like a queue and a dictionary among others. The authors also explain how they arrived at the contracts by applying their guidelines and principles. While reading, I sometimes thought, Well sure, everybody knows how a queue is supposed to work so that contract is easy. I wanted something a little more challenging. But I guess their point is more about expressing good contracts rather than figuring out what they should be.

An interesting concept explained in the book is that of "frame rules". Frame rules are simply the things that don't change as the result of a command. When writing software, I tend to think about what the code does, but it can also be worthwhile to consider what it doesn't do. For example, in the add method above, it would be undesirable if the items previously in the list were changed as a result of calling add. Frame rules would be postconditions that say that any existing list items are still in the list and in the same order as before add was called. Obviously you have to weigh the cost of extensively checking frame rules, both in terms of run-time performance as well as development time.

Speaking of performance, the authors give some advice about how to minimize the impact of run-time checks. First, put all the expensive stuff in postconditions and invariants. Then, once a class has been fairly well tested, turn those off, leaving just the precondition checking. This strategy is similar to what is recommended by Sun when using assertions in Java: Throw exceptions if a precondition is violated, and assert postconditions. Assertions can be turned off at any time to boost performance.

One trick for pushing expensive operations into postconditions is to introduce derived queries. For example, suppose you have a method with a precondition, and to check the precondition requires copying a large list of items. Often you can introduce a derived query to use in the method's precondition, where the derived query has a post condition that does the expensive list copy instead.

One of the most important things I learned from the book is simply that writing contracts is a realistic thing to do, once you know how. By that I mean it's kind of like my experience with unit testing. I knew about unit testing for a long time before I ever tried doing it. I always thought there would be lots of things that were hard to test, but once I started and learned the techniques, there weren't so many untestable things as I thought. Design by contract is similar: with some practice and knowledge of techniques, it is doable.