The notion of conformance

Having looked at a precise definition of contracts, we may well reflect (again) on the nature of object-oriented computation to draw implications with respect to system development. Each object, so to speak, carries a state and behavior. In principle, the state is not directly accessible but is encapsulated by a well-defined method interface. Computation in an object-oriented system, then, amounts to sending messages that may be dispatched by the dynamic binding mechanism to invoke the appropriate procedure. As a client of a server object, what you need to know is what you may expect as the result of invoking a method, irrespective of what actual procedure the message is dispatched to.

Object = state + behavior

Subtyping

Inheritance ( type hierarchy graph )


slide: The substitutability requirement

In many cases it is sufficient to characterize the desired behavior in terms of (behavioral) conformance. Operationally, (the behavior of) an object B conforms to (the behavior of) an object A if the behavior of B is in some sense equivalent to the behavior of object A. More formally, this may be expressed in terms of the subtype relation, by saying that type B is a subtype of A if any object of type B may be used anywhere that an object of type A may be used. Behavioral conformance amounts to the requirement of substitutability. See slide 3-conformance. If we know what a type does, we may substitute any object that realizes the type as long as it does what it is expected to do. In other words, conformance to a type means that the behavior of the object respects the constraints imposed by the type. A direct corollary of our notion of conformance is that instances of derived classes must conform to the type of their base classes. We will study how this affects the use of contracts to characterize the behavioral properties of a class.

System development

From the perspective of system development, the notion of contracts has some interesting consequences. Most importantly, contracts may be used to document the method interface of a class. Pre- and post-conditions allow the class designer to specify in a concise manner the functional characteristics of a method, whereas the use of natural language often leads to lengthy (and imprecise) descriptions. Below, an example is given of a contract specifying an account. With regard to encapsulation, the obvious disadvantage of using assertions is that detailed knowledge of the class, including knowledge of the existence of private and protected instance variables, is needed to be able to understand the meaning of these assertions. However, this may not be so odd as it appears, since we do regard an object as having a state and operations that possibly modify this state. From this point of view, encapsulation may be seen as merely prohibiting access to the state, not as a dictum not to look at or reason about the state. Nevertheless, despite the advantages the use of assertions offers from a formal point of view, the cognitive complexity of logical assertions will usually necessitate additional comments (in natural language) for a user to be able to understand the functionality of an object and the services it offers.

System development

A pre-condition limits the cases that a supplier must handle!


slide: System development with contracts

Assertions may be used to decide who is responsible for any erroneous behavior of the system. See slide 3-limits. For example, imagine that you are using a software library to implement a system for financial transactions and that your company suffers a number of losses due to bugs in the system. How would you find out whether the loss is your own fault or whether it is caused by some bug in the library? Perhaps surprisingly, the use of assertions allows you to determine exactly whether to sue the library vendor or not. Assume that the classes in the library are all annotated with assertions that can be checked dynamically at runtime. Now, when you replay the examples that resulted in a loss for your company with the option for checking pre- and post-conditions on, it can easily be decided who is in error. In the case a pre-condition of a method signals violation, you, as a client of a library object, are in error. However, when no pre-condition violation is signaled, but instead a post-condition is violated, then the library object as the supplier of a service is in error; and you may proceed to go to court, or do something less dramatic as asking the software vendor to correct the bug.

Realization

The contract specified in the class interface as given in slide 3-acc-1 may actually be enforced in the code as illustrated in slide 3-acc-2.

Realization

account

  class account {
  public:
  account() { _balance = 0; assert(invariant()); }
  
  virtual float balance() { return _balance; }
  
  
  void deposit(float x) {
  	require( x >= 0 );  
check precondition

hold();
to save the old state

_balance += x; promise( balance() == old_balance + x ); promise( invariant() ); } void withdraw(float x) { require( x <= _balance );
check precondition

hold();
to save the old state

_balance -= x; promise( balance() == old_balance - x ); promise( invariant() ); } virtual bool invariant() { return balance() >= 0; } protected: float _balance; float old_balance;
additional variable

virtual void hold() { old_balance = _balance; } };

slide: The realization of the $account$ contract

The additional variable {\em old_balance} is needed to compare the state preceding an operation with the state that results afterwards. The old state must explicitly be copied by calling hold. In this respect, Eiffel offers better support than C++. Whenever balance() proves to be less than zero, the procedure sketched above can be used to determine whether the error is caused by an erroneous method invocation, for example when calling withdraw(x) with x >= balance(), or whether the implementation code contains a bug. For the developer of the software, pre-conditions offer a means to limit the number of cases that a method must be able to handle. Often, programmers tend to anticipate all possible uses. For instance, many programs or systems have options that may be learned only when inspecting the source code but are otherwise undocumented. \nop{See for example [UndocDos].} Rather than providing all possible options, for now and the future, it is more sensible to delineate in a precise manner what input will be processed and what input is considered illegal. For the developer, this may significantly reduce the effort of producing the software. It is important that what is and what is not supported is in principle negotiable whenever the class interface explicitly states the requirements imposed on the user.