Contracts

To establish the interaction between objects in a more precise way, we need a notion of contracts, specifying the requirements a client must comply with when requesting a service from a server object. Our notion of contracts will be based on the notion of types. In the universe of programming, types are above all a means to create order and regularity. Also, in an object-oriented approach, types may play an important role in organizing classes and their relationships. As observed in  [HOB87], the notion of types gives a natural criterion for modularization, perhaps not so much as a guideline to arrive at a particular object decomposition, but as a means to judge whether the modular structure of a system is consistently defined, that is technically well-typed. See slide 3-types.

Object-Oriented Methodology

increases programmer productivity by enhancing software maintainability, extensibility and reusability

OOP blurs design and implementation


slide: Types and inheritance

The meaning of a type must be understood as a formal characterization of the behavior of the elements belonging to the type. A type consists of a (possibly infinite) collection of elements which is characterized by the definition of the type. For example, a class defines such a collection, namely the instances of the class, whose behavior is constrained by the specification of the class.

Formal specification

A formal specification of the behavior of an object may be given by defining a pre-condition and post-condition for each method. The pre-condition of a method specifies in a logical manner what restrictions the client invoking a particular method is obliged to comply with. When the client fails to meet these requirements the result of the method will be undefined. In effect, after the violation of a pre-condition anything can happen. Usually, this means that the computation may be aborted or that some other means of error-handling may be started. For instance, when the implementation language supports exceptions an exception handler may be invoked. The post-condition of a method states what obligations the server object has when executing the method, provided that the client's request satisfies the method's pre-condition. Apart from specifying a pre-condition and post-condition for each method publicly supported by the class, the designer of the class may also specify a class invariant, to define the invariant properties of the state of each instance of the class. A class annotated with an invariant and pre- and post-conditions for the methods may be regarded as a contract, since it specifies precisely (in an abstract way) the behavioral conformance conditions of the object and the constraints imposed on the interactions between the object and its clients. See slide 3-obligations.

Contractual obligations

clientsupplier
pre-conditionobligationbenefit
post-conditionbenefitobligation

slide: Contractual obligations


Assertions -- formal specification

  • require -- method call pre-condition
  • ensure, promise -- post-condition
  • invariant -- object invariance

slide: Formal specification of contracts

Intuitively, contracts have a clear analogy to our business affairs in everyday life. For instance, when buying audio equipment, as a client you wish to know what you get for the price you pay, whereas the dealer may require that you pay in cash. Following this metaphor through, we see that the supplier may actually benefit from imposing a (reasonable) pre-condition and that the client has an interest in a well-stated post-condition. Most people are not willing to pay without knowing what they will get for their money. The use of contracts was originally proposed by  [Meyer88], and is directly supported by the language Eiffel, which offers the keywords require (to indicate a pre-condition), ensure (to indicate a post-condition) and invariant (to indicate the invariance condition). See slide 3-formal. The Eiffel environment has options to dynamically check any of the three kinds of assertions, even selectively per class. The assertions, except for the invariance condition, are directly embedded in the code. Although less elegant, the same functionality can be achieved in C++ by using the assert macro defined in {\tt assert.h} as explained in section ASSERT, which also introduced the require and promise macros for C++. For dynamically checking the invariance condition, a test should be executed when evaluating the constructor and before and after each method invocation. While a method is being executed, the invariant need not necessarily hold, but it is the responsibility of a method to restore the invariant when it is disrupted. In case object methods are recursively applied, the invariant must be restored when returning to the original caller. An alternative approach to incorporating assertions in a class description is presented in  [Cline], which introduces an extension of C++ called Annotated C++. Instead of directly embedding assertions in the code, Annotated C++ requires the user to specify separately the axioms characterizing the functionality of the methods and their effect on the state of the object. Important from a design perspective is that the specification, even without an implementation of the methods, may be understood as specifying a contract. In practice, they may only be stated as comments.

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

  • computation is sending messages

Subtyping

  • if B is a subtype of A then B may be used wherever A is used

Inheritance ( type hierarchy graph )

  • code sharing (storage representation + operations)

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.

Contract -- interface

account

  class account {
  public:
  account();
  // assert( invariant() );
  
  virtual float balance() { return _balance; }
  
  void deposit(float x); 
\c{// to deposit money}
// require( x >= 0 ); // promise( balance() == old_balance + x && invariant() ); void withdraw(float x);
\c{// to withdraw money}
// require( x <= balance() ); // promise( balance() == old_balance - x && invariant() ); bool invariant() { return balance() >= 0; } protected: float _balance; };

slide: The $account$ contract

The interface for the account class given in slide 3-acc-1, specifies in an abstract way what the user expects of an account. From the perspective of design, the behavioral abstraction expressed by the axioms is exactly what we need, in principle. The implementation must guarantee that these constraints are met. 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

  • violated pre-condition -- bug in client
  • violated post-condition -- bug in supplier

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.

Realization versus refinement

Inheritance provides a convenient mechanism to factor out code and to use this code as basic building blocks from which the system may be composed. The difference between classes and procedures in this respect is the support offered by classes for encapsulation and modularization. In addition, inheritance is an important mechanism for structuring the design specification. From the perspective of design, inheritance may be regarded as determining the type structure of the system. A type defines in an abstract way a collection of objects with similar behavior. This collection includes the collections of objects defined by its subtypes. A subtype may be regarded as imposing additional constraints and hence as corresponding to a generally smaller and more precisely delineated collection of objects.

Type = objects with similar behavior

  • partial types are designed to have subtypes

Inheritance -- factor out code (building blocks)

  • abstract interface -- implementation is left to subtypes
  • type hierarchy -- behavioral refinement and extension

slide: Partial types

An important notion for understanding the use of types in an object oriented context is the notion of partial types as introduced in  [HOB87]. See slide 3-partial. Partial types are types designed to have subtypes! Other authors speak of abstract types or abstract classes (cf. Stroustrup, 1988; Meyer, 1988). A partial type (or abstract class) specifies the interface of an object (class), and hence of all its subtypes (subclasses), without necessarily providing a full implementation. The actual realization of the type is left to the object classes implementing a subtype. As an example of a partial type, think of an interface specification of a stack. An abstract class may specify the interface to the stack and derived classes may specify a variety of implementations using, for example, fixed length arrays, linked lists or dynamic arrays. Another example of the use of partial types has already been given in section inheritance, when defining a collection of graphical shapes. The realization of the abstract class shape is left to the derived classes, such as circle and rectangle, which are sufficiently concrete to provide an implementation for the method draw. Note that there is a significant difference between the two examples given. In the example of a stack, a realization amounts simply to providing an implementation. In the second example, however, the various realizations of the abstract graphical shape immediately correspond to a type hierarchy of graphical shapes. The concrete graphical shapes are refinements of the abstract class shape, and may further be refined into classes describing more specific graphical shapes. For instance, the class rectangle may be used as a base class for the class square. It is also possible that a derived class extends the interface of the base class, as has occurred for the compound shape class.

Issues -- delegation versus inheritance

  • applicability -- how relevant to the type?
  • complexity -- how difficult to understand and implement?
  • reusability -- how (re)useful is the code?
  • implementation -- how dependent?

slide: Delegation versus inheritance

A number of issues play a role in deciding where to put the functionality when constructing a (refinement) type hierarchy. See slide 3-issues. First, it must be decided whether inheritance is the proper mechanism to use. Often, the reuse of code is more safely effected when using delegation or forwarding to an embedded object, instead of inheritance. See section del-inh. Inheritance should only be used when it can be shown that there is actually a (behavioral) refinement relation satisfying the constraints outlined below. Whether particular code belongs to a type obviously depends upon whether the functionality expressed by the code is relevant to the type. Another criterion concerns the complexity of the resulting type structure, including both the cognitive complexity and the complexity of implementing it. In other words, how difficult is it to understand, respectively implement, the type hierarchy? More particularly, what tricks, such as explicit type conversions (casts) or the bypassing of encapsulation (friends), must be used to realize the desired functionality in code? Another important criterion is whether the type structure (and code) is easily reusable. How much effort is involved in adapting the structure to more specific needs? Admittedly, the solution to these issues will depend upon the application. However, the constraints that follow from extending the notion of contracts to include classes derived by inheritance do provide a (minimal) guideline for designing a well-behaved type structure.

Contracts and inheritance

Contracts provide a means to specify the behavior of an object in a formal way by using logical assertions. In particular, a contract specifies the constraints involved in the interaction between a server object and a client invoking a method for that object. When developing a refinement subtype hierarchy we need to establish that the derived types satisfy the constraints imposed by the contract associated with the base type. To establish that the contract of a derived class refines the contract of the base class it suffices to verify that the following rules are satisfied. See slide 3-inheritance.

Refining a contract -- state responsibilities and obligations

  • invariance -- the invariants of all the parents of a class apply to the class itself
  • methods -- services may be added or refined

Improving a service -- do only more than expected

  • accept weaker pre-conditions
  • guarantee stronger post-conditions

slide: Contracts and inheritance

First, the invariant of the base class must apply to all instances of the derived class. In other words, the invariance assertions of the derived class must be logically equal to or stronger than the assertions characterizing the invariant properties of the base class. This requirement may be verified by checking that the invariance properties of the base class can be logically derived from the statement asserting the invariance properties of the derived class. The intuition underlying this requirement is that the behavior of the derived class is more tightly defined and hence subject to stronger invariance conditions. Secondly, each method occurring in the base class must occur in the derived class, possibly in a refined form. Note that from a type theoretical point of view it is perfectly all right to add methods but strictly forbidden to delete methods, since deleting a method would violate the requirement of behavioral conformance that adheres to the subtype relation. Apart from adding a method, we may also refine existing methods. Refining a method involves strengthening the post-condition and weakening the pre-condition. Suppose that we have a class C derived from a base class P, to verify that the method m_C refines the method m_P defined for the base class P, we must check, assuming that the signatures of m_C and m_P are compatible, that the post-condition of m_C is not weaker than the post-condition of m_P, and also that the pre-condition of m_C is not stronger than the pre-condition of m_P. See slide 3-refining.

Refining a method -- like improving a business contract

  class C : public P { ... virtual void m(); ... }
  
  • pre( m_C ) >= pre(m_P) \zline{weaken pre-condition}
  • post( m_C ) <= post(m_P) \zline{strengthen post-condition}

slide: Refining a method

This rule may at first sight be surprising, because of the asymmetric way in which post-conditions and pre-conditions are treated. But reflecting on what it means to improve a service, the intuition underlying this rule, and in particular the contra-variant relation between the pre-conditions involved, is quite straightforward. To improve or refine a service, in our common sense notion of a service, means that the quality of the product or the result delivered becomes better. Alternatively, a service may be considered as improved when, even with the result remaining the same, the cost of the service is decreased. In other words, a service is improved if either the client may have higher expectations of the result or the requirements on the client becomes less stringent. The or is non-exclusive. A derived class may improve a service while at the same time imposing fewer constraints on the clients.

Example

As an example of improving a contract, consider the refinement of the class account into a class {\em credit_account}, which allows a consumer to overdraw an account to a limit of some maximum amount. See slide 3-credit-account.

Refining the account contract

\zline{\fbox{C++}}
  class credit_account : public account {
  public:
  credit_account(float x) { _maxcredit = x; _credit = 0; }
  
  float balance() { return _balance + _credit; }
  
  float credit(float x) { 
  	require( x + _credit <= _maxcredit );
  	hold();
  	_credit += x;
  	promise( _credit = old_credit + x );
  	promise( _balance = old_balance);
  	promise( invariant() );
  	}
  
  void reduce(float x) {
  	require( 0 <= x && x <= _credit );
  	hold();
  	_credit -= x;
  	promise( _credit = old_credit - x );
  	promise( _balance = old_balance );
  	promise( invariant() );
  	}
  
  
  bool invariant() {
  	return _credit <= _maxcredit && account::invariant();
  	}
  protected:
  float _maxcredit, _credit;
  float old_credit;
  void hold() { old_credit = _credit; account::hold(); }
  };
  

slide: Refining the account contract

As a first observation, we may note that the invariant of account immediately follows from the invariant of credit_account. Also, we may easily establish that the pre-condition of withdraw has (implicitly) been weakened, since we are allowed to overdraw the {\em credit_account} by the amount given by credit. Note, however, that this is implied by the virtual definition of balance(). To manage the credit given, the methods credit and reduce are supplied. This allows us to leave the methods deposit and withdraw unmodified.

Delegation or inheritance

Object-oriented languages offer a variety of constructs that may be used to implement a system. In particular, when developing a class, the designer has the choice between developing the class anew, deriving the class using inheritance, incorporating an (object of an) existing class to which messages may be delegated, or instantiating a given template class with an appropriate type parameter. The choice may be difficult, since there are a number of trade-offs involved. For instance, when using explicit delegation instead of inheritance, each indirection must be explicitly coded. When using inheritance, derived code can immediately be used, but the interface of the derived class may get too `fat'. The actual choice made will often be dependent on the functionality offered by the class libraries available. For example, only a few of the C++ libraries employ templates. See section libraries. In the absence of template classes, the designer may be forced to use inheritance and type insecure casts to mimic generic data types such as lists. In  [HOB87], guidelines are given on how and when to use inheritance. Their view is in accord with a design methodology centered around subtyping, that is the derivation of new object types within the constraints of behavioral conformance. See slide 3-design-methodology.

Subtyping (design methodology)

  • stepwise refinement by specialization

Specialization -- conceptual hierarchies
Implementation -- to realize a supertype
Combination -- multiple inheritance

Non-standard subtyping:

Generalization -- Win -> ColorWin
Variance -- Mouse -> Tablet [Pointing]
Limitation -- Deque -> Stack
Creativity and clear headed thinking are the most important ingredients of design!


slide: Design methodology

As we have seen previously, subtyping may be used to realize a partially defined (abstract) type, or as a means with which to add a specialization to a conceptual hierarchy. On a number of occasions, however, the need may arise to employ what  [HOB87] called non-standard subtyping, and which is also known as non-strict inheritance. An example of non-strict inheritance is a derivation that results in a generalization, as occurs, for instance, when deriving a colored window class from a base class window (supporting only black and white). We speak of a generalization since the type associated with colored windows properly encompasses the collection of black and white windows. Yet from an implementation perspective, it may be convenient to simply extend the code written for black and white windows, instead of redesigning the class hierarchy. Another example of non-strict inheritance is when the derivation results in what may be called a variant type, which occurs, for instance, when deriving a pen or tablet class from a class defining the behavior of a mouse. The proper course of action in such cases is to introduce an abstract class pointing device from which both the mouse and tablet classes may be derived (as proper subtypes). Another, equally evident, violation of the subtyping regime occurs when restricting a given class to achieve the functionality belonging to the type of the derived class. A violation, since the requirement of behavioral conformance demands that either new behavior is added or already existing behavior is refined. Actual limitation of behavior results in a behaviorally incompatible class. The standard example of limitation, originally given in  [Snyder86], is the restriction of a double ended queue into a stack. A better, and from a type perspective, perfectly legal solution is to derive a stack from a double ended queue by using private inheritance. However, from a design perspective, the use of private inheritance must be considered ill-practice, since dependencies may be introduced in the object model that remain invisible in a more abstract (type oriented) description of the system. Delegation, as in the handler/body idiom described in section 2-handler, is to be preferred in such cases since it makes the uses relation explicit and does not compromise the type system.

Example

When developing a class structure, often the choice will arise as to whether to employ inheritance or explicit delegation as a means to utilize the functionality of a particular class.

Resource sharing -- delegation or inheritance

  • class line : public list { ... }
  • class line : private list { ... }
  • class line { list words; ... }

slide: Sharing resources

The choices a designer may be confronted with when developing a system or a class library may be illustrated by a simple example. Suppose you must develop a system that produces a kwicindex. A kwicindex contains for each line of text all possible rotations, so that each word can be rapidly searched for, together with the context in which it occurs. The example is taken from  [VanVliet], who discusses a number of alternative solutions from the perspective of information hiding. An object-oriented solution appears to be surprisingly simple, in particular when a suitable library is available. The two classes that are of interest are, respectively, a class line, to represent each line of text, and a class index to represent the lines generated for the kwicindex.
  

slide: Representing lines

The class line is simply a list of words (strings) that can be rotated. The actual implementation of a line is provided by an instantiation of a generic (template) class list, that is contained as a data member, to which the operations insert and rotate are delegated. Instead of incorporating a member of type list, the line class could have been made a descendant of the class list, as indicated below
  class line : public list {
  ...
  };
  
However, this would make the type line a subtype of list, which does not seem to be a wise decision. Alternatively, private inheritance could have been employed as in
  class line : private list {
  ...
  };
  
But, as we have noted before, private inheritance may also introduce unwanted dependencies. Explicit delegation seems to be the best solution, because the relation between a line object and a list of strings is best characterized as a uses relation. }
  

slide: Generating the index

For the index class, a similar argument as for the line class can be given as to why delegation is used instead of public or private inheritance. Actually, the most difficult part of the code is the function defining how to compare two lines of text that is needed for sorting the index. Comparison is based on the lexicographical order as defined below.
  
The ease of implementing both the line and index classes depends largely upon the availability of a powerful list template class. In the example, we employed the functions list::rotate to rotate the elements in the list, and list::sort to sort the elements in the list. The function list::compare was used to determine the <= relation between elements of the list. The actual library employed for this example is the splash library, which is discussed in section libraries.