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 of B
  • In any case in which an instance of A is called for, then an instance of B 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 WHATZITs?

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.

Note: When a class implements a version of an inherited feature which was deferred in its parent, this is known as "effecting" the feature. Because features being effected are getting their first implementation, it is not necessary to list their names in the redefine part, or anywhere else in the inheritance part of the heir.

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 RECTANGLEs 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 RECTANGLEs.

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) endIn 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.

cached: 03/19/2024 12:34:43.000 AM