Keywords: conformance, deferred, effective, dynamic type, dynamic dispatch, polymorphism
A class in an inheritance hierarchy has many types, from the current type to the most abstract type at the top of the hierarchy. A common pattern is to define the feature interface at a high level in the hierarchy, and to defer the body or action of a feature until a later, descendant class. Such a deferred feature has its name and signature defined in a parent, but the definition or body of the feature is given in the child. The child effects or gives an effective definition for the feature, so each child can define its own specific action. Objects of the various child types can be treated identically by calling the feature on the object; the feature interface is identical, but the content of the feature is specific to each class. This technique is called polymorphism, because it allows the same code to use objects of many shapes or types.
11.1 The Eiffel type hierarchy
Eiffel uses a class hierarchy to define the very top, and the very bottom levels of any user-defined class. At the top of the Eiffel hierarchy is the class GENERAL, that defines the generic routines clone, copy, and is_equal. Below that is the class PLATFORM, that defines the specific features needed to tailor the Eiffel language to run on a specific platform or computer system, such as the number of bits used to store numbers of type INTEGER, REAL, and DOUBLE. Below that is the class ANY, which is the ancestor of every user-defined class, so all user-defined classes sit below ANY in the hierarchy.
Every class written by a user inherits from the Eiffel class ANY, without the need for an explicit inheritance clause. The class ANY allows features to be defined that work for any class; more formally, that work for all classes of type ANY. System-wide features can be defined once at the appropriate level, and used by all the children of class ANY; in particular, the generic features clone, copy, and is_equal are defined once and used in any class. No class inherits NONE, by definition. Class NONE defines the bottom node in the inheritance hierarchy, so by definition no class can inherit it. The special value Void is of type NONE.
With the introduction of inheritance, what is meant by the type of an object has to be defined in more detail. A worker is a person; more formally, an object of type WORKER is also an object of type PERSON, because WORKER inherits PERSON. The base class of a specific class is defined to be the top-level user class, at the top of the user-defined hierarchy. The inheritance hierarchy for a class may be many levels deep, so a class can be of many types, from its current type all the way up to type ANY. In the previous chapter, an inheritance hierarchy was defined for a CONTRACTOR, who is a WORKER, who is a PERSON, who is of type ANY. This reflects the real world: I am a worker, a person, a human being, an animal, a living being, and a thing (in some languages, the top level of the inheritance hierarchy is a class THING). An object that is an instance of a class in an inheritance hierarchy is of many types. A class has an immediate type, as well as one or more inherited types. A generic class can generate many types, one type for each actual parameter.
Given the Eiffel class hierarchy, in particular the two classes ANY and NONE, it can now be seen that there is a single, consistent rule for export policies; a feature is exported to the classes listed in its export policy. A feature exported to objects of type ANY is thus available to all classes; a feature exported to objects of type NONE is available to no classes. A feature exported to one or more specific classes is available to those classes, and to descendants of those classes, because a descendant of class X is of type X, as well as more specific types.
Inheritance in the Eiffel type system provides a way to define one type in terms of another. It also determines when one type can replace another, and ensures that the type system works in an intuitively reasonable manner. Consider an example where you go into a restaurant and ask for a salad. For this request, it is reasonable that any type of salad will be acceptable, such as a garden salad, a Waldorf salad, a chef's salad, and so on. These are specific types of salad, so they conform to the definition of a salad. On the other hand, receiving a hamburger would be a surprise, because a hamburger is not a type of salad. The notion of conformance makes this expectation explicit.
In an assignment statement, the type of the right hand side must be the same as the type of the variable on the left hand side. Because a variable may have many types due to inheritance, a more formal rule must be given: the type of the expression on the right of the assignment must conform to the type on the left. Class B conforms to class A if they are the same class, or class B is a descendant of A; A cannot also conform to B, unless they are the same type.
Consider the classes CONTRACTOR, WORKER, and PERSON,
a hierarchy of three classes. A contractor is a type of worker, and a worker
is a type of person. If I need a job done and advertise for the services
of a worker, then there is no surprise if I use a contractor, because a
contractor can take the place of a worker. On the other hand, I would be
surprised if a person, who is not a worker, answered the ad; I want a more
specific class. In the same way, an Eiffel variable can treat an object
of a parent type in the same way it treats an object of a subtype, because
the child does at least as much as the parent, and possibly more. One type
can be used in place of another if they conform.
p: PERSON p := c -- valid; a contractor is a person
w: WORKER p := w -- valid; a worker is a person
c: CONTRACTOR w := p -- invalid; a person is
not a worker
w := c -- valid; a contractor is a worker
c := p -- invalid; a person is not a contractor
c := w -- invalid; a worker is not a contractor
When a feature is redefined, the signature of the new version must conform to the signature of its precursor. The signature of a feature lists the number, order and type of the values passed as arguments to the routine, and the type of any value returned from the routine. To ensure that redefinition works in a reasonable manner, the signatures of any old and new versions of a feature must conform; formally, each type in the signature of the new version must conform to the type in the old version. A redefined feature often keeps the original signature; conformance allows descendants to replace their ancestors in the signature.
An expanded type conforms directly to its base type, and indirectly to other classes through the base type. Consider the two declarations and the assignment
ref_type: T
exp_type: expanded T
ref_type := exp_type
This assignment is valid, and has the effect of copying the values of exp_type into the variable ref_type. Expanded types are discussed in more detail in Meyer (1992).
The foundation of reusable software is to define a feature once, and use the feature as needed. One of the most powerful inheritance techniques is to define the feature interface for a general class of objects in a general class, and then leave each specific, inheriting class to define its own specific action or body of the feature. Every child plays the same role and has similar behaviour, but the exact, internal details of the behaviour are different.
The routine header defines the interface to the routine, and the routine body defines the action. A routine may be defined with a header, but no body; such a routine cannot be executed. The interface is defined in a parent class, and the body is deferred; a child class then inherits the routine, and defines the body. The parent defines a deferred routine, and the child makes this routine effective; such a process is called effecting the routine.
A deferred routine is defined by replacing the keyword do with the keyword deferred, and leaving the body of the routine empty. A deferred routine to find an area, for example, is
deferred class X
feature
area: REAL is
-- area
deferred
end
A class with a deferred routine is called a deferred class; this is stated in the class header. An instance of a deferred class cannot be created, because Eiffel cannot find a routine body to connect to the header. A deferred class therefore does not contain a creation clause. An instance of a child class can be created if the child effects every deferred routine, so there are no deferred routines in the child.
Common error: forget to state that class is deferred, or forget to effect a feature
Error code: VCCH (1)
Error: Class has deferred feature(s), but is not declared as deferred.
What to do: make feature(s) effective, or include "deferred" before "class" in Class_header
Common error: Try to create an object of a deferred type. Either you need to make the deferred class effective by effecting every deferred feature, or you need to use an effective child of the deferred class.
Error code: VGCC (2)
Type error: creation instruction applies to target of a deferred type
What to do: make sure that type of target is effective.
11.4 A deferred example: class
POLYGON
Consider a system that implements a graphics library. Classes in the library define geometric shapes such as points, lines, circles, triangles, squares, and rectangles. A polygon is a general name for closed geometric objects made of straight lines, such as triangles and squares. An abstract class POLYGON may therefore be defined to capture the general properties and behaviour of polygons; this class is then inherited by specific types of polygon.
Operations on polygons include computing the area and perimeter of a shape, moving the shape around, or changing the size of the shape. These behaviours can be defined in the abstract class POLYGON as effective or as deferred routines. A polygon has a perimeter, and the length of this perimeter can be easily calculated for arbitrary polygons, so an effective routine to calculate the perimeter is defined in this class. A polygon has an area, but it is difficult to define a method for finding the area of an arbitrary polygon, so the body of this feature is deferred. Defining the function area as a deferred routine says that all polygons have an area, but the effective routine to calculate the area is left for more specific classes to define.
The code for the deferred class POLYGON looks like
deferred class POLYGON
feature {NONE}
vertices: LINKED_LIST[POINT]
feature {ANY}
make is
-- get the points that define the polygon
deferred
end -- make
perimeter: REAL is
-- the length of the perimeter of the polygon
local this, previous:
POINT
do
from
vertices.start
this := vertices.item
until vertices.islast
loop
previous := this
vertices.forth
this := vertices.item
Result := Result + this.distance (previous)
end
Result := Result + this.distance (vertices.first)
end -- perimeter
area: REAL is
-- return the area of the figure
deferred
end -- area
display is
-- display the location of the vertices
do
from vertices.start
until vertices.after
loop
vertices.item.display
vertices.forth
end
end -- display
move (delta_x, delta_y: REAL) is
-- move by delta_x horizontally and delta_y vertically
do
from vertices.start
until vertices.after
loop
vertices.item.move (delta_x, delta_y)
vertices.forth
end
end -- move
end -- class POLYGON
Specific types of polygon, such as triangles, rectangles and squares, define effective versions for each deferred routine. A deferred routine is not redefined in the child, because it was never defined; the child defines an effective routine. An effective parent routine may be used by the child as written, or they may be redefined to more specific versions.
11.5 An effective example: class
RECTANGLE
A rectangle is a polygon with four sides, where the sides meet at right angles. Rectangles are created, moved, and displayed like any other polygon, and have an area and perimeter. On the other hand, a rectangle has special features of its own (matching sides, four vertices, right angles) which may result in better ways to do some of the operations. RECTANGLE can thus be defined as a child of POLYGON, the inherited features can be effected or changed, and new features can be added as needed. To create a rectangle, all the RECTANGLE routines have to be effective, because it is impossible to create an object of a deferred type. One possible way to implement the class RECTANGLE is
class RECTANGLE
inherit
POLYGON
redefine perimeter
end
creation
make
feature { NONE}
number_of_vertices:
INTEGER is 4
side1, side2:
REAL
feature {ANY}
make
is
-- make a rectangle, store the lengths of the sides
local
i: INTEGER
p: POINT
do
!!vertices.make
io.putstring ("%NEnter the four points of the rectangle")
from
until i = number_of_vertices
loop
!!p.make
vertices.extend (p)
&nb