Inheritance
Inheritance, along with client/supplier, are the two relationships that can exist between classes.
Inheritance lets us mirror in software the types of abstractions that are common in many problem domains, i.e., the more general to the more specialized.
Inheritance also gives a way us to combine these abstractions.
Inheritance allows us to make extensions and adaptations to existing software, while at the same time, leaving the original software unaltered.
The Eiffel Inheritance Model
If class B
inherits from class A
, then:
- Every feature of
A
is also a feature ofB
- In any case in which an instance of
A
is called for, then an instance ofB
will suffice.
Flexibility and adaptability are key qualities of the Eiffel inheritance model. On an informal level, this means that, except as prevented by certain constraints, a class can inherit from a set of classes containing just about any other classes.
Eiffel classes can be effective or deferred. If a class is effective, then it is completely implemented. As a result, it is possible to create and use direct instances of an effective class at runtime.
If a class is deferred, then it is not completely implemented. A class is deferred if it contains at least one deferred feature. So, it is possible for you to mark a feature (and by consequence also its class) as deferred when you code it. This means that the specification for this class dictates that such a feature exists, but there is no implementation for the feature included in the class. As a result, there can be no direct instances of deferred classes at runtime. However, a class that inherits from a deferred class can implement, or effect, the deferred features. This results in an effective descendant to the deferred class. And it is possible to create direct instances of this effective descendant. Such instances would also be instances (albeit not direct instances) of the original deferred class.
What this means to us as software producers, is that in any development effort, we have available a great number of classes which can serve as potential starting points. That is, classes that we could make parents to the classes we produce. And, those classes do not have to chosen from a strict dichotomy of classes which are either completely abstract or completely implemented. Inheritance from classes that are deferred but have some implemented features is both possible and encouraged. It reuses existing software and it reduces the opportunity for error.
Consider the deferred class COMPARABLE
from the Eiffel Base Library. A portion of COMPARABLE
is shown below: deferred class
COMPARABLE
feature -- Comparison
is_less alias "<" (other: like Current): BOOLEAN
-- Is current object less than `other'?
deferred
end
is_less_equal alias "<=" (other: like Current): BOOLEAN
-- Is current object less than or equal to `other'?
do
Result := not (other < Current)
end
is_greater alias ">" (other: like Current): BOOLEAN
-- Is current object greater than `other'?
do
Result := other < Current
end
is_greater_equal alias ">=" (other: like Current): BOOLEAN
-- Is current object greater than or equal to `other'?
do
Result := not (Current < other)
end
is_equal (other: like Current): BOOLEAN
-- Is `other' attached to an object of the same type
-- as current object and identical to it?
do
Result := (not (Current < other) and not (other < Current))
end
If you are producing a class that you wish to support basic comparison operators, like "<" and ">", you can have that class inherit from COMPARABLE
, which has features which correspond to those operators. The text for COMPARABLE
contains eight features. Seven of these are effective and one is deferred.
So through inheritance from COMPARABLE
, your class, let's call it WHATZIT
, would now have these features available. But how would the features of COMPARABLE
know what it means to compare WHATZIT
s?
Of course, it would have no way of knowing, so you must show it. And you do that by writing the implementation for "<", the one deferred feature that WHATZIT
inherits from the COMPARABLE
class.
When you look closely at the effective features of COMPARABLE
, you see that their implementations are ultimately based on "<". If we were not able to inherit from multiple partially implemented classes, then we would be forced to implement many more features, a process which invites error, or, in the case of comparison, to move to a less appealing model.
The Inheritance Part of Classes in Eiffel
Because the inheritance model has such flexibility, it must also have adaptability. A consequence of inheriting from multiple classes is that it would be possible to inherit multiple features with the same name ... and you remember from Adding Class Features that a class is not allowed to have more than one feature with the same name. A process called feature adaptation allows us to resolve these issues in an heir. Feature adaptation is also done for reasons other than resolving name clashes as well.
Feature adaptation is an enabling capability, but it is also one that takes some study to understand fully.
We will look at the types of feature adaptation that will serve most useful to you as you begin to produce Eiffel software.
In Eiffel Classes you saw where the inheritance part fits into the class structure. Shown below is a portion of class LINKED_QUEUE
from the Eiffel libraries. LINKED_QUEUE
is an effective class which implements the abstract notion of a QUEUE
(a deferred class) with an implementation based on the services provided by LINKED_LIST
(an effective class). class
LINKED_QUEUE [G]
inherit
QUEUE [G]
undefine
is_empty,
copy,
is_equal
redefine
linear_representation,
prune_all,
extend
select
item,
put
end
LINKED_LIST [G]
rename
item as ll_item,
remove as ll_remove,
make as ll_make,
remove_left as remove,
put as ll_put
export
{NONE}
all
{ANY}
writable,
extendible,
wipe_out,
readable
undefine
fill,
append,
prune,
readable,
writable,
prune_all,
extend,
force,
is_inserted
redefine
duplicate,
linear_representation
select
remove
end
Okay ... now calm down ... please. This is an example from a very highly-evolved and sophisticated library which is replete with software reuse. LINKED_QUEUE
has two parents and uses considerable feature adaptation. In fact, it uses every feature adaptation option available. The benefit is obvious, though. LINKED_QUEUE
class has only seven features actually coded. In total there are only 26 lines of instructions!
In practice you can use inheritance, even multiple inheritance, to do some quite productive programming in Eiffel without having to write anything that looks like the inheritance part of LINKED_QUEUE
above.
Regardless, let's break LINKED_QUEUE
's inheritance part into chunks and take a look at some of them.
Rename
rename
item as ll_item,
remove as ll_remove,
make as ll_make,
remove_left as remove,
put as ll_put
As you might have already guessed, the rename part, introduced oddly enough by the keyword "rename
", is used to rename features.
Specifically, it is used when an heir wants to use a feature from a parent, but wants to use it under a different name than that by which the parent knows it. So in the example, the feature known as item
in LINKED_LIST
is perfectly usable in LINKED_QUEUE
, but must be applied as ll_item
.
This is common when your class inherits two different features with the same name from two different parents and you want to be able to use them both. Because you can only have one feature with a given name, then rename one of the features.
New Exports
export
{NONE}
all
{ANY}
writable,
extendible,
wipe_out,
readable
The new exports part is introduced by the keyword "export
". This section allows you to change the export status of inherited features. Remember from Adding Class Features that features become available (or not) to clients by their export status. Export status of immediate features is controlled in the feature clause. But here we are dealing with inherited features, so we control their status in the export part of the class's inheritance section. Any feature not mentioned will have the same export status as it did in the parent class.
In this example, the keyword "all
" is used first to say that all features inherited form LINKED_LIST
are unavailable to any clients (export to class NONE
). This is typical for a class like LINKED_QUEUE
in which the features important to the client come from the deferred parent, in this case QUEUE
, and the class LINKED_LIST
is used only for implementation. But, it seems that also in this case, the producer felt differently about the features writable
, extendible
, wipe_out
, and readable
, and decided the allow clients of ANY
type to utilize these features inherited from LINKED_LIST
.
Undefine
undefine
is_empty,
copy,
is_equal
Next, undefine ... it's probably not what you think. You might assume that undefine is a way to banish forever any inherited features that you just do not want to deal with. But what happens to features whose names are listed in an undefine clause is that they become deferred features in the heir.
Undefine is useful if you inherit two different features of the same name from different parents, a situation you cannot live with. If you like one and you don't like the other, then you can undefine the one you don't like. The the only version you get is the one you like.
Another way you might use undefine is in the case in which you actually want a feature to be deferred in an heir that was effective in a parent.
Redefine
redefine
linear_representation,
prune_all,
extend
The redefine part lists the names of effective features for which the producer of the heir class would like to provide implementations that replace the inherited implementations.
So, in this example the implementation for linear_representation
, for example, that LINKED_QUEUE
would have inherited from QUEUE
will not be used. Instead LINKED_QUEUE
implements its own version of linear_representation
.
Select
select
remove
The select part is used only under special circumstances. The case in which select is required involves a situation called "repeated" inheritance. Repeated inheritance occurs when an heir inherits more than once from the same ancestor. Usually this means it has two or more parents who have a common proper ancestor (but it can occur directly). The features from the common ancestor are inherited by each of the parents and passed on to the heir. The rules and effects of repeated inheritance occupy an entire chapter in the official Eiffel programming language reference and will not be reproduced here. Just understand at this point that it is sometimes necessary to use select
to provide the dynamic binding system with an unambiguous choice of features in the presence of polymorphic attachment.
You should note also that repeated inheritance can and does occur often without causing any problem at all. In fact it happens in every case of multiple inheritance, due to the fact that all classes inherit from class ANY and receive its features as a result. The reason it is not a problem is that in the case that any feature makes it from the original common ancestor along multiple paths to the heir with its name and implementation still intact, it will arrive as only one feature heir. This is called sharing and nothing special needs to be done to make it happen.
Polymorphism
It is time now to see another way in which inheritance helps build more extendible software.
Assume that we have to build classes that model different types of polygons. We would do this by building a class for polygon which would model a garden-variety polygon, a multi-sided closed figure. But when we consider that there are specialized types of polygons, like triangles and rectangles, we realize that to support these specializations, we need classes for them as well. And this is an obvious opportunity for inheritance. All triangles and rectangles are polygons. So, we start with class POLYGON
and its proper descendants TRIANGLE
and RECTANGLE
.
So we can make declarations like: my_polygon: POLYGON
your_polygon: POLYGON
my_triangle: TRIANGLE
my_rectangle: RECTANGLE
another_rectangle: RECTANGLE
Assume these declarations are in force for all the examples this section on polymorphism.
We saw in Adding Class Features that we can say that one class conforms to another if it is the same class or one of its proper descendants. Therefore POLYGON conforms to POLYGON
. Also, TRIANGLE
and RECTANGLE
conform to POLYGON
. But, importantly, POLYGON
does not conform to TRIANGLE
or RECTANGLE
. This makes sense intuitively, because we know all rectangles and triangles are polygons ... and we also know that not all polygons are rectangles.
Polymorphic Attachment
These facts affect how assignments can work. Using the declarations above: my_polygon := your_polygon -- Is valid
your_polygon :=my_polygon -- Is valid
my_polygon :=my_rectangle -- Is valid
my_polygon := my_triangle -- Is valid
but my_rectangle := my_polygon -- Is not valid
my_triangle := my_polygon -- Is not valid
and of course my_rectangle := my_triangle -- Is not valid
Consider now the assignment below which is valid. my_polygon := my_rectangle
After an assignment like this executes the entity my_polygon
will be holding at runtime a reference to an instance of a type which is not a direct instance of its declared type POLYGON
. But conformance ensures us that, although it may not be a direct instance, it will indeed by an instance. (all rectangles are polygons).
Depending upon how many different types of polygons get modeled in classes, the entity "my_polygon
" could be attached objects of may different types ... it could take on many forms. This in fact is the basis for the term "polymorphism"; having many forms. So we speak of "polymorphic attachment" as the process by which at runtime entities can hold references to objects which are not of the entity's declared type ... but they are of conforming types.
Now let's see how we get some value from this.
Dynamic Binding
Suppose that one of the features of POLYGON
is a query perimeter
which returns an instance's perimeter. The producer of POLYGON
may have implemented perimeter
as a function that computes the perimeter by adding up the lengths of all the sides. This approach is guaranteed to work for all polygons, and we can apply the perimeter
feature to any polygon. Let's print some perimeters: print (my_polygon.perimeter)
print (my_triangle.perimeter)
print (my_rectangle.perimeter)
TRIANGLE
and RECTANGLE
might have properties, expressed as queries, which as a part of their specialization, distinguish them from run-of-the-mill polygons. Two features of rectangles are width
and height
the lengths of the sides.
Armed with these RECTANGLE
-specific features, the producer of RECTANGLE
may say, "Now I no longer have to depend upon that crude implementation of perimeter
that is inherited from POLYGON
. I can build an efficient RECTANGLE
-specific implementation of perimeter
, based on the knowledge that for all RECTANGLE
s perimeter = 2*(width+height)"
To implement this specialized version of perimeter
, the producer of RECTANGLE
must add the feature to the class, but also must list its name in the "redefine
" part of the RECTANGLE
's inheritance clause. class
RECTANGLE
inherit
POLYGON
redefine
perimeter
end
.
.
feature
perimeter: REAL
-- Sum of lengths of all sides
do
Result := 2 * (width + height)
end
You would expect then, that this version of perimeter would be executed in the following context: print (my_rectangle.perimeter)
But what makes this interesting is that even in the context below my_polygon := my_rectangle
print (my_polygon.perimeter)
in which perimeter
is being applied to a entity declared as POLYGON
, the specialized version of perimeter
from RECTANGLE
is being used. It would be impossible to ensure at compile time which version of perimeter
is most appropriate. So it must be done at runtime. This ability to choose the best version of a feature to apply, just at the moment it needs to be applied, is called "dynamic binding".
Static typing tells us at compile time that it is safe to apply perimeter
to my_polygon
No matter which of the types of polygons is attached to my_polygon
, there will be a perimeter
feature that will work.
Dynamic binding tells us that when we apply perimeter
, we know that the most appropriate version of the feature will get applied at runtime.
Object Test
Now let's add another situation. Consider the code below: my_polygon := my_rectangle
print (my_polygon.perimeter)
print (my_polygon.width) -- Is invalid
We could apply perimeter
to my_polygon
and everything is fine ... we even get RECTANGLE
's specialized version of the feature. But it is invalid for us to try to apply width
to my_polygon
even though we feel (with rather strong conviction) that at this point in execution, my_polygon
will be attached to an object of type RECTANGLE
, and we know that width
is a valid query on RECTANGLE
s.
The reason follows. When we declared my_polygon
as type POLYGON
, we made a deal that says that the only features that can be applied to my_polygon
are the features of POLYGON
. Remember that static typing guarantees us at compile time that at runtime there will be at least one version of the feature available that can be applied. print (my_polygon.width) -- Is invalid
But in the case above, the guarantee cannot be made. my_polygon
is declared with class POLYGON
which has no width
feature, despite the fact that some of its proper descendants might.
Does this mean that we can never do RECTANGLE
things with this instance again, once we have attached it to my_polygon
?
No. There is a language facility called the object test which will come to our rescue. The object test will allow us safely to attach our instance back to an entity typed as RECTANGLE
. After doing so, we are free use RECTANGLE
features. my_polygon := my_rectangle
print (my_polygon.perimeter)
if attached {RECTANGLE} my_polygon as l_rect then
print (l_rect.width)
end
In this code, the entity l_rect
is a fresh local entity produced during the object test. So, the code can be read: if at this point, my_polygon
is attached to an instance of type RECTANGLE
, then attach that instance to a fresh local entity named l_rect
, then apply width
to l_rect
and print the result.
Note: The object test replaces the functionality of an obsolete mechanism called assignment attempt. Assignment attempt used the syntax
?=
in the context of assignment versus the:=
of normal assignment.