Extreme Design by Contract Yishai A. Feldman Efi Arazi School of Computer Science The Interdisciplinary Center, P.O.B. 167, Herzliya 46150, Israel +972-9-9527305,
[email protected], http://www.idc.ac.il/yishai
ABSTRACT Design by contract is a practical technique for developing code together with its (light-weight and executable) specification. It is synergistic with several XP practices, particularly unit testing and refactoring. This paper investigates this relationship and points out how it can be automated (at least in part). Keywords Refactoring, design by contract, tool support. 1 INTRODUCTION Design by contract [6] is a practical methodology for developing object-oriented programs together with their specifications. It offers immediate benefits in terms of early error detection as well as long-term process improvement. The specifications are given as part of the code itself in the form of assertions such as class invariants and method pre- and postconditions. This allows the compiler to instrument the program to check these assertions at runtime. No less important, it allows the programmer to reason about the program with relative ease. The first language to support design by contract was Eiffel, but the idea is gaining in popularity and several tools [1, 4, 5, 7] have emerged recently for using this methodology when programming in Java. As implemented in these languages, the assertion language is limited in its expressive power, and does not support the full generality needed for program verification. However, it is easy to use and still offers substantial practical benefits. Design by contract is synergistic with several XP practices, particularly unit testing and refactoring. This paper investigates this relationship, with a detailed analysis of the interactions between design-by-contract and refactoring. This analysis specifies the type of interaction as well as the difficulty of automating the contract modifications implied by the refactorings. 2 CONTRACTS AND XP The implementation of a new piece of functionality is considered to be finished in XP when all the unit tests written to define it execute successfully. This practice works well when the unit tests are well-written, but no tests can completely specify the desired functionality,
and it is certainly possible for a bug to slip through. Also, as code is changed (during refactoring or while adding functionality), it is possible that existing tests become invalid because they refer to an old partition of responsibilities between classes or methods, which is no longer valid. This means that unit tests must continuously be maintained, together with the code they test. Contracts are an alternative way of specifying functionality. It takes some getting used to, but experienced developers find it very natural to write a contract before the implementation of a class or method. Contracts are still not enough for testing, since additional code needs to be written to exercise the methods. However, this code is concerned with covering all interesting paths through the program rather than in checking the correctness of the results—this is the responsibility of the contracts. This has two benefits. First, less testing code needs to be written; this is like XP unit tests with the calls that check results removed. Second, this means that testing code can be written in larger units that exercise several methods simultaneously, instead of separate tests for each method. These two observations, taken together, mean that unit tests need to be modified less frequently when they rely on contracts for checking correctness of the results. Of course, the contract itself needs to change together with the changes in the functionality of the code. This is easier than changing the tests, since the contract gives the meaning of the class and methods, and changes we want to make in the code are naturally reflected in the contract. (Actually, this puts the cart before the horse; we should first understand what we are trying to do in terms of the contract, then translate that understanding into changes in the code.) Furthermore, contract modifications can be done in a systematic way, in the same way that refactorings are defined systematically. The rest of this paper discusses the interactions between refactoring and contracts. 3 CONTRACTS AND REFACTORING: AN EXAMPLE As explained above, refactorings are closely tied with contracts; whenever functionality is redistributed between methods and classes, contracts are intimately
involved. One refactoring, Introduce Assertion, is all about contracts. There are a number of additional contract-related refactorings that need to be introduced, such as Pull Up Contract, Pull Down Contract, and Introduce Abstract Precondition. However, we now focus on refactorings that involve both contracts and code.
returns stack.getElements(). In this case, the reference to the former elements field is treated just as it would in the code itself. In addition, the original invariant in RPNCalc that referred to the elements field is now moved to Stack. Now we have several methods in RPNCalc that refer to the vector through the pointer to the Stack object. We now use Move Method to move size, display (now renamed to top), and enter (renamed to push) to the Stack class. The original method bodies now delegate to the methods in Stack, and their contracts are copied to the new methods.
We first illustrate some of the interactions between contracts and refactoring by means of an example. Starting with a Java class, RPNCalc, which emulates a simple RPN calculator (such as HP used to make), a series of steps is used to extract a general stack class. The original program provides the following operations: size() returns the number of elements in the operand stack; display() and second() return the top element and the next one, respectively (these are called x and y in HP calculators); enter(int n) pushes a new number onto the operand stack; and add() and sub() perform the arithmetic operations on the top two elements in the operand stack. The original implementation uses the Java Vector class for the operand stack. We are assuming that the original class is already equipped with a contract, and we will follow the evolution of this contract through the refactoring process. An excerpt from this program appears in Fig. 1 at the end of the paper. Missing is most of the code, as well as some of the contract. (The full set of programs is available from http://www.faculty.idc.ac.il/ yishai/xp-contracts.htm.) The contract is expressed in the syntax of JMSAssert [7]; as in similar tools, assertions appear in the Javadoc comments of the classes or methods they belong to.
The original implementation of add and sub made use of the methods of Vector, which give full access to all elements. These methods therefore perform the required operation on the last two elements of the vector, and then reduced the size of the vector by one using the method setSize. As a step toward the elimination of outside access to the vector, which now resides in Stack, we use Extract Method to encapsulate the reduction in size by a new method, called pop. This new method needs a contract, but this contract is harder to find. We can reason as follows. First, since the body of this method sets the size of the vector to size() - 1, and the size of a vector may not be negative, we need a precondition size() >= 1. Second, a postcondition can be added stating that the value returned by size() after this operation, which is computed as the size of the vector, is one less than its previous value. Finally, the value now returned by display() is the one previously returned by second(). These reasoning steps were presented in order of increasing sophistication; it is possible that they can be discovered automatically, and we are developing a tool for this purpose [2]; however, this will require sophisticated tools that rely on some theorem-proving capability. After extracting this method, it is moved to Stack.
In the first step, Extract Class is used to define a new class called Stack, which is initially empty. A new final field, stack, is added to RPNCalc, which is initialized in the constructor. At this point, an invariant is added to RPNCalc, stating that this field can never be null. Next, Self-Encapsulate Field is used to encapsulate all accesses to the field, elements, that contains the vector in RPNCalc via a getter method getElements. A new invariant is added, stating that getElements never returns null. This directly follows from the invariant of RPNCalc that states that the elements field is never null. In addition, getElements is equipped with a postcondition that states that it returns the value of elements.
We can now use Substitute Algorithm to complete the refactoring of add and sub, by rewriting their bodies to make use of push and pop. At this point, there is no further use of getElements in RPNCalc, and this method, together with the associated contract (including the invariant) can be removed. We now have a full-functionality stack class, which we can use in other applications. Unlike traditional stack implementations, this provides access to the second element as well as the top one. We can consider generalizing this to allow read-only access to all elements of the stack (while leaving the LIFO insertion and removal policy). However, this would be adding new functionality, and is therefore outside the scope of
Next, Move Field is used to move the elements field to the Stack class (and a getter method is added for it there). In addition to the obvious changes, the postcondition on getElements now needs to say that it
2
this discussion. Excerpts from the results of the whole transformation process appears in Figs. 2 and 3 at the end of the paper.
when refactoring are very simple; for example, the contract of the isNull method created by Introduce Null Object specifies that it returns true for the class that represents the null object, and false otherwise. Such a contract can be created automatically by a refactoring tool. Other contracts can be derived from existing contracts; for example, the contract of a delegating method is easily adapted from that of the method it delegates to. As we saw in the example, in some cases it is more difficult to automate the creation of the new contract. In particular, Extract Method can take a piece of code of arbitrary complexity and make it into a new method. Even in such cases, ongoing research [2] indicates that some automation is possible, but this is much more difficult and requires careful human supervision.
It should be noted that the testing code for RPNCalc (not shown here) did not need to be modified in any way during this process. Of course, the new Stack class requires tests of its own; these are easily modified from those of RPNCalc. 4 DETAILED ANALYSIS I have analyzed the interactions between design-bycontract and each of the 68 refactorings mentioned in Chapters 6–11 of Fowler’s book [3]. Of course, on the simplest level, contracts are treated just like code. For example, when renaming a method, all references to it in the code must be appropriately modified; so must all references in assertions. Similarly, a method may be eliminated when it is not used anywhere, including in assertions. This is taken for granted in the analysis, and is not counted as an interaction between contracts as refactoring.
As mentioned above, in some cases it is necessary to check for constraint violations. These checks can be syntactic in nature, in which case they are easy to automate. In other cases, they might require stronger theorem-proving mechanisms.
Interaction Statistics It turns out that about 32% of the refactorings studied do not interact with contracts (except in the simple fashion mentioned above). These are mostly syntactic refactorings such as Remove Parameter or Rename Method, or those that eliminate classes or methods, such as Inline Class and Inline Method. About 54% of the refactorings require the addition of a contract for new methods or classes that they create; an additional 4% require some contract modifications or movement.
Apart from the 35% of the refactorings studied that did not interact with contracts, 38% required interactions that were judged to be easy to automate. An additional 17% were judged difficult to automate, although partial help is possible using the methods of Feldman and Gendler [2]. Another 10% would require theorem proving for constraint checking. 5 CONCLUSION We have seen an example of a series of refactorings with associated contract modifications. Some of these were mechanical and easy to automate, while others required deeper understanding of the application. In any case, the understanding embodied in the contract is helpful for refactoring with confidence.
Contracts affect the applicability of some 14% of the refactorings studied. (This number includes 4% that also require contract additions and were also counted above.) For example, each new method introduced into an existing class must obey its invariant. (Of course, it is possible that the invariant itself is at fault and should be modified. This would be done in a separate step prior to the introduction of the offending method.) The design-by-contract methodology imposes strict constraints on the use of contracts by subclasses, all of which follow from the requirement that classes may not violate the contract of their superclasses. (Violations of these constraints would inevitably create a bug when an instance of the subclass is used polymorphically as an element of the superclass.) As a result of these constraints, it is necessary to check for possible violations before applying refactorings such as Move Method and Extract Superclass.
I believe that design by contract is an essential technique for producing high-quality code, and is synergistic with XP and other agile methodologies. It simplifies the creation of test cases, clarifies the design, prevents some common bugs (mostly related to the correct use of inheritance), and can even be automated to some extent. I hope this paper will encourage XP practitioners to try using it in large-scale projects. REFERENCES [1] Parasoft Corp. Jcontract home page. http: //www.parasoft.com/jsp/products/ home.jsp?product=Jcontract.
Automation Some of these new contracts that need to be added
[2] Y. A. Feldman and L. Gendler. Automatic discovery of software contracts. In progress. 3
[3] M. Fowler. Refactoring: Improving the Design of Existing Code. Addison-Wesley, 2000.
/** @inv stack != null * @inv size() >= 0 */ public class RPNCalc { final protected Stack stack;
[4] R. Kramer. iContract home page. http://www.reliable-systems.com/ tools/iContract/iContract.htm. [5] R. Kramer. iContract—the Java design by contract tool. In Proc. Technology of Object-Oriented Languages and Systems, TOOLS-USA. IEEE Press, 1998.
/** @post size() == $prev(size()) + 1 * @post display() == n */ public void enter(int n) { stack.push(n); }
[6] B. Meyer. Object-Oriented Software Construction. Prentice Hall, 2nd edition, 1997.
/** @pre size() >= 2 * @post size() == $prev(size()) - 1 * @post display() == $prev(second()) * + $prev(display()) */ public void add() { int n2 = stack.top(); stack.pop(); int n1 = stack.top(); stack.pop(); stack.push(n1 + n2); }
[7] Man Machine Systems. Design by contract tool for Java—JMSAssert. http://www. mmsindia.com/JMSAssert.html. /** @inv elements != null * @inv size() >= 0 */ public class RPNCalc { final protected Vector elements; }
/** @pre size() >= 1 */ public int display() {...}
Figure 2: Final program—RPNCalc (excerpt).
/** @post size() == $prev(size()) + 1 * @post display() == n */ public void enter(int n) { elements.addElement(new Integer(n)); }
/** @inv elements != null * @inv size() >= 0 */ public class Stack { final protected Vector elements; /** @pre size() >= 1 */ public int top() {...}
/** @pre size() >= 2 * @post size() == $prev(size()) - 1 * @post display() == $prev(second()) * + $prev(display()) */ public void add() { int size = elements.size(); elements.setElementAt (new Integer(second() + display()), size - 2); elements.setSize(size - 1); }
/** @post size() == $prev(size()) + 1 * @post top() == n */ public void push(int n) { elements.addElement(new Integer(n)); } /** @pre size() >= 1 * @post size() == $prev(size()) - 1 */ public void pop() { elements.setSize(elements.size() - 1); }
}
Figure 1: Original program (excerpt). }
Figure 3: Final program—Stack (excerpt). 4