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

Inheritance -- factor out code (building blocks)


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


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.