Inheritance is a powerful and attractive technique. A look at either the practice or literature shows, however, that it is not always well applied. Eiffel has made a particular effort to tame inheritance for the benefit of modelers and software developers. Many of the techniques are original with Eiffel. Paul Dubois has written ( comp.lang.python Usenet newsgroup, 23 March 1997): there are two things that [Eiffel] got right that nobody else got right anywhere else: support for design by contract, and multiple inheritance. Everyone should understand these "correct answers" if only to understand how to work around the limitations in other languages .
To make a class inherit from another, simply use an inherit clause:
This makes D an heir of A , B and any other class listed. Eiffel supports multiple inheritance: a class may have as many parents as it needs. Later sections ( "Multiple inheritance and renaming", page 64 and "Repeated inheritance and selection", page 73 ) will explain how to handle possible conflicts between parent features.
This discussion will rely on the terminology introduced on page 14 : descendants of a class are the class itself, its heirs, the heirs of its heirs and so on. Proper descendants exclude the class itself. The reverse notions are ancestors and proper ancestors .
By default D will simply include all the original features of A , B , …, to which it may add its own through its feature clauses if any. But the inheritance mechanism is more flexible, allowing D to adapt the inherited features in many ways. Each parent name -- A , B , … in the example -- can be followed by a Feature Adaptation clause, with subclauses, all optional, introduced by keywords rename , export , undefine , redefine and select , enabling the author of A to make the best use of the inheritance mechanism by tuning the inherited features to the precise needs of D . This makes inheritance a principal tool in the Eiffel process, mentioned earlier, of carefully crafting each individual class, like a machine, for the benefit of its clients. The next sections review the various Feature Adaptation subclauses.
The first form of feature adaptation is the ability to change the implementation of an inherited feature.
Assume a class SAVINGS_ACCOUNT that specializes the notion of account. It is probably appropriate to define it as an heir to class ACCOUNT , to benefit from all the features of ACCOUNT still applicable to savings accounts, and to reflect the conceptual relationship between the two types: every savings account, apart from its own specific properties, also "is" an account. But we may need to produce a different effect for procedure deposit which, besides recording the deposit and updating the balance, may also need, for example, to update the interest.
This example is typical of the form of reuse promoted by inheritance and crucial to effective reusability in software: the case of reuse with adaptation . Traditional forms of reuse are all-or-nothing: either you take a component exactly as it is, or you build your own. Inheritance will get us out of this "reuse or redo" dilemma by allowing us to reuse and redo. The mechanism is feature redefinition:
Without the redefine subclause, the declaration of deposit would be invalid, yielding two features of the same name, the inherited one and the new one. The subclause makes this valid by specifying that the new declaration will override the old one.
In a redefinition, the original version -- such as the ACCOUNT implementation of deposit in this example -- is called the precursor of the new version. It is common for a redefinition to rely on the precursor's algorithm and add some other actions; the reserved word Precursor helps achieve this goal simply. Permitted only in a routine redefinition, it denotes the parent routine being redefined. So here he body of the new deposit (called " New implementation " above) could be of the form
Besides changing the implementation of a routine, a redefinition can turn an argument-less function into an attribute; for example a proper descendant of ACCOUNT could redefine deposits_count , originally a function, as an attribute. The Uniform Access Principle (page 19 ) guarantees that the redefinition makes no change for clients, which will continue to use the feature under the form acc . deposits_count .
The inheritance mechanism is relevant to both roles of classes: module and type. Its application as a mechanism to reuse, adapt and extend features from one class to another, as just seen, covers its role as a module extension mechanism. But it's also a subtyping mechanism. To say that D is an heir of A , or more generally a descendant of A , is to expresses that instances of D can be viewed as instances of A .
Polymorphic assignment supports this second role. In an assignment x := y , the types of x and y do not have, with inheritance, to be identical; the rule is that the type of y must simply conform to the type of x . A class D conforms to a class A if and only if it is a descendant of A (which includes the case in which A and D are the same class); if these classes are generic, conformance of D [ U ] to C [ T ] requires in addition that type U conform to type T (through the recursive application of the same rules).
In addition, it follows from the earlier discussion of tuples ( "Tuple types", page 90 ), that TUPLE [ X ] conforms to TUPLE, TUPLE [ X, Y, ] to TUPLE [ X ] and so on.
So with the inheritance structure that we have seen, the declarations
make it valid to write the assignment
which will assign to acc a reference attached (if not void) to a direct instance of type SAVINGS_ACCOUNT , not ACCOUNT .
Such an assignment, where the source and target types are different, is said to be polymorphic. An entity such as acc , which as a result of such assignments may become attached at run time to objects of types other than the one declared for it, is itself called a polymorphic entity.
For polymorphism to respect the reliability requirements of Eiffel, it must be controlled by the type system and enable static type checking. We certainly do not want an entity of type ACCOUNT to become attached to an object of type DEPOSIT . Hence the second typing rule:
An assignment x := y , or the use of y as actual argument corresponding to the formal argument x in a routine call, is only valid if the type of y conforms to the the type of x . |
The second case listed in the rule is a call such as target . routine ( …, y , … ) where the routine declaration is of the form routine ( …, x : SOME_TYPE , … ) . The relationship between y , the actual argument in the call, and the corresponding formal argument x , is exactly the same as in an assignment x := y : not just the type rule, as expressed by Type Conformance (the type of y must conform to SOME_TYPE ), but also the actual run-time effect which, as for assignments, will be either a reference attachment or, for expanded types, a copy.
The ability to accept the assignment x := Void for x of any reference type ( "Basic operations", page 28 ) is a consequence of the Type Conformance rule, since Void is of type NONE which by construction ( "The global inheritance structure", page 15 ) conforms to all types.
Polymorphism also yields a more precise definition of "instance". A direct instance of a type A is an object created from the exact pattern defined by the declaration of A 's base class, with one field for each of the class attributes; you will obtain it through a creation instruction of the form create x …, for x of type A , or by cloning an existing direct instance. An instance of A is a direct instance of any type conforming to A : A itself, but also any type based on descendant classes. So an instance of SAVINGS_ACCOUNT is also an instance, although not a direct instance, of ACCOUNT .
A consequence of polymorphism is the ability to define polymorphic data structures . With a declaration such as
the procedure call accounts . extend ( acc ), because it uses a procedure extend which in this case expects an argument of any type conforming to ACCOUNT , will be valid not only if acc is of type ACCOUNT but also if it is of a descendant type such as SAVINGS_ACCOUNT . Successive calls of this kind make it possible to construct a data structure that, at run-time, might contain objects of several types, all conforming to ACCOUNT :
Such polymorphic data structures combine the flexibility and safety of genericity and inheritance. You can make them more or less general by choosing for the actual generic parameter, here ACCOUNT , a type higher or lower in the inheritance hierarchy. Static typing is again essential here, prohibiting for example a mistaken insertion of the form accounts . extend ( dep ) where dep is of type DEPOSIT , which does not conform to ACCOUNT .
At the higher (most abstract) end of the spectrum, you can produce an unrestrictedly polymorphic data structure general_list : LIST [ ANY ] which makes the call general_list . extend ( x ) valid for any x . The price to pay is that retrieving an element from such a structure will yield an object on which the only known applicable operations are the most general ones, valid for all types: assignment, copy, clone, equality comparison and others from ANY . Assignment attempt, studied below, will make it possible to apply more specific operations after checking dynamically that a retrieved object is of the appropriate type.
The complement of polymorphism is dynamic binding, the answer to the question "What version of a feature will be applied in a call whose target is polymorphic?".
Consider acc is of type ACCOUNT . Thanks to polymorphism, an object attached to acc may be a direct instance not just of ACCOUNT but also of SAVINGS_ACCOUNT or other descendants. Some of these descendants, indeed SAVINGS_ACCOUNT among them, redefine features such as deposit . Then we have to ask what the effect will be for a call of the form
Dynamic binding is the clearly correct answer: the call will execute the version of deposit from the generating class of the object attached to acc at run time. If acc is attached to a direct instance of ACCOUNT , execution will use the original ACCOUNT version; if acc is attached to a direct instance of SAVINGS_ACCOUNT , the call will execute the version redefined in that class.
This is a clear correctness requirement. A policy of static binding (as available for example in C++ or Delphi, for non-virtual functions) would take the declaration of acc as an ACCOUNT literally. But that declaration is only meant to ensure generality, to enable the use of a single entity acc in many different cases: what counts at execution time is the object that acc represents. Applying the ACCOUNT version to a SAVINGS_ACCOUNT object would be wrong, possibly leading in particular to objects that violate the invariant of their own generating class (since there is no reason a routine of ACCOUNT will preserve the specific invariant of a proper descendant such as SAVINGS_ACCOUNT , which it does not even know about).
In some cases, the choice between static and dynamic binding does not matter: this is the case for example if a call's target is not polymorphic, or if the feature of the call is redefined nowhere in the system. In such cases the use of static binding permits slightly faster calls (since the feature is known at compile time). This application of static binding should, however, be treated as a compiler optimization . The EiffelStudio compiler, under its "finalization" mode, which performs extensive optimization, will detect some of these cases and process them accordingly -- unlike approaches that make developers responsible for specifying what should be static and what dynamic (a tedious and error-prone task, especially delicate because a minute change in the software can make a static call, in a far-away module of a large system, suddenly become dynamic). Eiffel programmers don't need to worry about such aspects; they can rely on the semantics of dynamic binding in all cases, with the knowledge that the compiler will apply static binding when safe and desirable.
Even in cases that require dynamic binding, the design of Eiffel, in particular the typing rules, enable compilers to make the penalty over the static-binding calls of traditional approaches very small and, most importantly, constant-bounded : it does not grow with the depth or complexity of the inheritance structure. The discovery in 1985 of a technique for constant-time dynamic binding calls, even in the presence of multiple and repeated inheritance, was the event that gave the green light to the development of Eiffel.
Dynamic binding is particularly interesting for polymorphic data structures. If you iterate over the list of accounts of various kinds, accounts : LIST [ ACCOUNT ] , illustrated in the last figure, and at each step let acc represent the current list element, you can repeatedly apply
to have the appropriate variant of the deposit operation triggered for each element.
The benefit of such techniques appears clearly if we compare them with the traditional way to address such needs: using multi-branch discriminating instructions of the form if " Account is a savings account " then … elseif " It is a money market account " then … and so on, or the corresponding case … of …, switch or inspect instructions. Apart from their heaviness and complexity, such solutions cause many components of a software system to rely on the knowledge of the exact set of variants available for a certain notion, such as bank account. Then any addition, change or removal of variants can cause a ripple of changes throughout the architecture. This is one of the majors obstacles to extendibility and reusability in traditional approaches. In contrast, using the combination of inheritance, redefinition, polymorphism and dynamic binding makes it possible to have a point of single choice -- a unique location in the system which knows the exhaustive list of variants. Every client then manipulates entities of the most general type, ACCOUNT , through dynamically bound calls of the form acc . some_account_ feature (…).
These observations make dynamic binding appear for what it is: not an implementation mechanism, but an architectural technique that plays a key role (along with information hiding, which it extends, and Design by Contract, to which it is linked through the assertion redefinition rules seen below) in providing the modular system architectures of Eiffel, the basis for the method's approach to reusability and extendibility. These properties apply as early as analysis and modeling, and continue to be useful throughout the subsequent steps.
The examples of dynamic binding seen so far assumed that all classes were fully implemented, and dynamically bound features had a version in every relevant class, including the most general ones such as ACCOUNT .
It is also useful to define classes that leave the implementation of some of their features entirely to proper descendants. Such an abstract class is known as deferred ; so are its unimplemented features. The reverse of deferred is effective , meaning fully implemented.
LIST is a typical example of deferred class. As it describes the general notion of list, it should not favor any particular implementation; that will be the task of its effective descendants, such as LINKED_LIST (linked implementation), TWO_WAY_LIST (linked both ways) , ARRAYED_LIST (implementation by an array), all effective, and all indeed to be found in EiffelBase.
At the level of the deferred class LIST , some features such as extend (add an item at the end of the list) will have no implementation and hence will be declared as deferred. Here is the corresponding form, illustrating the syntax for both deferred classes and their deferred features:
description: "Sequential finite lists, without a commitment%[ |
A deferred feature (considered to be a routine, although it can yield an attribute in a proper descendant) has the single keyword deferred in lieu of the do Instructions clause of an effective routine. A deferred class -- defined as a class that has at least one deferred feature -- must be introduced by deferred class instead of just class .
As the example of extend shows, a deferred feature, although it has no implementation, can be equipped with assertions. They will be binding on implementations in descendants, in a way to be explained below.
Deferred classes do not have to be fully deferred. They may contain some effective features along with their deferred ones. Here, for example, we may express count as a function:
This implementation relies on the loop construct described below ( from introduces the loop initialization) and on a set of deferred features of the class which allow traversal of a list based on moving a fictitious cursor: start to bring the cursor to the first element if any, after to find out whether all relevant elements have been seen, and forth (with precondition not after ) to advance the cursor to the next element. Procedure forth itself appears as
where index -- another deferred feature -- is the integer position of the cursor.
Although the above version of feature count is time-consuming -- it implies a whole traversal just for the purpose of determining the number of elements -- it has the advantage of being applicable to all variants, without any commitment to a choice of implementation, as would follow for example if we decided to treat count as an attribute. Proper descendants can always redefine count for more efficiency.
Function count illustrates one of the most important contributions of the method to reusability: the ability to define behavior classes that capture common behaviors (such as count) while leaving the details of the behaviors (such as start , after , forth ) open to many variants. As noted earlier, traditional approaches to reusability provide closed reusable components. A component such as LIST , although equipped with directly usable behaviors such as count, is open to many variations, to be provided by proper descendants.
Some O-O languages support only the two extremes: fully effective classes, and fully deferred "interfaces", but not classes with a mix of effective and deferred features. This is an unacceptable limitation, negating the object-oriented method's support for a seamless, continuous spectrum from the most abstract to the most concrete.
A class B inheriting from a deferred class A may provide implementations -- effective declarations -- for the features inherited in deferred form. In this case there is no need for a redefine subclause; the effective versions simply replace the inherited versions. The class is said to effect the corresponding features. If after this process there remain any deferred features, B is still considered deferred, even if it introduces no deferred features of its own, and must be declared as deferred class .
In the example, classes such as LINKED_LIST and ARRAYED_LIST will effect all the deferred features they inherit from LIST -- extend , start etc. -- and hence will be effective.
Except in some applications restricted to pure system modeling -- as discussed next -- the main benefit of deferred classes and features comes from polymorphism and dynamic binding. Because extend has no implementation in class LIST , a call of the form my_list . extend ( … ) with my_list of type LIST [T] for some T can only be executed if my_list is attached to a direct instance of an effective proper descendant of LIST , such as LINKED_LIST ; then it will use the corresponding version of extend . Static binding would not even make sense here.
Even an effective feature of LIST such as count may depend on deferred features (start and so on), so that a call of the form my_list . count can only be executed in the context of an effective descendant.
All this indicates that a deferred class must have no direct instance . (It will have instances, the direct instances of its effective descendants.) If it had any, we could call deferred features on them, leading to execution-time impossibility. The rule that achieves this goal is simple: if the base type of x is a deferred class, no creation instruction of target x , of the form create x …, is permitted.
Deferred classes cover abstract notions with many possible variants. They are widely used in Eiffel where they cover various needs:
Capturing high-level classes, with common behaviors.
Defining the higher levels of a general taxonomy, especially in the inheritance structure of a library.
Defining the components of an architecture during system design, without commitment to a final implementation.
Describing domain-specific concepts in analysis and modeling.
These applications make deferred classes a central tool of the Eiffel method's support for seamlessness and reversibility. The last one in particular uses deferred classes and features to model objects from an application domain, without any commitment to implementation, design, or even software (and computers). Deferred classes are the ideal tool here: they express the properties of the domain's abstractions, without any temptation of implementation bias, yet with the precision afforded by type declarations, inheritance structures (to record classifications of the domain concepts), and contracts to express the abstract properties of the objects being described.
Rather than using a separate method and notation for analysis and design, this apprroach integrates seamlessly with the subsequent phases (assuming the decision is indeed taken to develop a software system): it suffices to refine the deferred classes progressively by introducing effective elements, either by modifying the classes themselves, or by introducing design- and implementation-oriented descendants. In the resulting system, the classes that played an important role for analysis, and are the most meaningful for customers, will remain important; as we have seen ( "Seamlessness and reversibility", page 9 ) this direct mapping property is a great help for extendibility.
The following sketch (from the book Object-Oriented Software Construction ) illustrates these ideas on the example of scheduling the programs of a TV station. This is pure modeling of an application domain; no computers or software are involved yet. The class describes the notion of program segment.
Note the use of assertions to define semantic properties of the class, its instances and its features. Although often presented as high-level, most object-oriented analysis methods (with the exception of Waldén's and Nerson's Business Object Notation) have no support for the expression of such properties, limiting themselves instead to the description of broad structural relationships.
Some deferred classes describe a structural property, useful to the description of many other classes. Typical examples are classes of the Kernel Library in EiffelBase:
NUMERIC describes objects on which arithmetic operations +, -, ∗, / are available, with the properties of a ring (associativity, distributivity, zero elements etc.). Kernel Library classes such as INTEGER and REAL -- but not, for example, STRING -- are descendants of NUMERIC . An application that defines a class MATRIX may also make it a descendant of NUMERIC .
COMPARABLE describes objects on which comparison operations <, <=, >, >= are available, with the properties of a total preorder (transitivity, irreflexivity). Kernel Library classes such as CHARACTER , STRING and INTEGER -- but not out MATRIX example -- are descendants of NUMERIC .
For such classes it is again essential to permit effective features in a deferred class, and to include assertions. For example class COMPARABLE declares infix "<" as deferred, and expresses > , >= and <= effectively in terms of it.
The type like Current will be explained in "Covariance and anchored declarations", page 79 ; you may understand it, in the following class, as equivalent to COMPARABLE .
It is often necessary to define a new class in terms of several existing ones. For example:
The Kernel Library classes INTEGER and REAL must inherit from both NUMERIC and COMPARABLE .
A class TENNIS_PLAYER , in a system for keeping track of player ranking, will inherit from COMPARABLE , as well as from other domain-specific classes.
A class COMPANY_PLANE may inherit from both PLANE and ASSET .
Class ARRAYED_LIST , describing an implementation of lists through arrays, may inherit from both LIST and ARRAY .
In all such cases multiple inheritance provides the answer.
Multiple inheritance can cause name clashes : two parents may include a feature with the same name. This would conflict with the ban on name overloading within a class -- the rule that no two features of a class may have the same name. Eiffel provides a simple way to remove the name clash at the point of inheritance through the rename subclause, as in
Here both LIST and ARRAY have features called count and item . To make the new class valid, we give new names to the features inherited from ARRAY , which will be known within ARRAYED_LIST as capacity and array_item . Of course we could have renamed the LIST versions instead, or renamed along both inheritance branches.
Every feature of a class has a final name : for a feature introduced in the class itself ("immediate" feature) it is the name appearing in the declaration; for an inherited feature that is not renamed, it is the feature's name in the parent; for a renamed feature, it is the name resulting from the renaming. This definition yields a precise statement of the rule against in-class overloading:
It is interesting to compare renaming and redefinition. The principal distinction is between features and feature names. Renaming keeps a feature, but changes its name. Redefinition keeps the name, but changes the feature. In some cases, it is of course appropriate to do both.
Renaming is interesting even in the absence of name clashes. A class may inherit from a parent a feature which it finds useful for its purposes, but whose name, appropriate for the context of the parent, is not consistent with the context of the heir. This is the case with ARRAY 's feature count in the last example: the feature that defines the number of items in an array -- the total number of available entries -- becomes, for an arrayed list, the maximum number of list items; the truly interesting indication of the number of items is the count of how many items have been inserted in the list, as given by feature count from LIST . But even if we did not have a name clash because of the two inherited count features we should rename ARRAY 's count as capacity to maintain the consistency of the local feature terminology.
The rename subclause appears before all the other feature adaptation subclauses -- redefine already seen, and the remaining ones export , undefine and select -- since an inherited feature that has been renamed sheds its earlier identity once and for all: within the class, and to its own clients and descendants, it will be known solely through the new name. The original name has simply disappeared from the name space. This is essential to the view of classes presented earlier: self-contained, consistent abstractions prepared carefully for the greatest enjoyment of clients and descendants.
A proper understanding of inheritance requires looking at the mechanism in the framework of Design by Contract, where it will appear as a form of subcontracting .
The first rule is that invariants accumulate down an inheritance structure:
The invariant of a class is automatically considered to include -- in the sense of logical "and" -- the invariants of all its parents. This is a consequence of the view of inheritance as an "is" relation: if we may consider every instance of B as an instance of A , then every consistency constraint on instances of A must also apply to instances of B .
Next we consider routine preconditions and postconditions. The rule here will follow from an examination of what contracts mean in the presence of polymorphism and dynamic binding.
Consider a parent A and a proper descendant B (a direct heir on the following figure), which redefines a routine r inherited from A .
As a result of dynamic binding, a call a1 . r from a client C may be serviced not by A 's version of r but by B 's version if a1 , although declared of type A , becomes at run time attached to an instance of B . This shows the combination of inheritance, redefinition, polymorphism and dynamic binding as providing a form of subcontracting; A subcontracts certain calls to B .
The problem is to keep subcontractors honest. Assuming preconditions and postconditions as shown on the last figure, a call in C of the form
or just a1 . q ; a1 . r where the postcondition of q implies the precondition pre of r , satisfies the terms of the contract and hence is entitled to being handled correctly -- to terminate in a state satisfying a1 . post . But if we let the subcontractor B redefine the assertions to arbitrary pre ' and post' , this is not necessarily the case: pre' could be stronger than pre , enabling B not to process correctly certain calls that are correct from A 's perspective; and post' could be weaker than post , enabling B to do less of a job than advertized for r in the Contract Form of A , the only official reference for authors of client classes such as C . (An assertion p is stronger than or equal to an assertion q if p implies q in the sense of boolean implication.)
The rule, then, is that for the redefinition to be correct the new precondition pre' must be weaker than or equal to the original pre , and the new postcondition post' must be stronger than or equal to the original post' .
Because it is impossible to check simply that an assertion is weaker or stronger than another, the language rule relies on different forms of the assertion constructs, require else and ensure then , for redeclared routines. They rely on the mathematical property that, for any assertions p and q , p implies ( p or q ) , and ( p and q ) implies p . For a precondition, using require else with a new assertion will perform an or , which can only weaken the original; for a postcondition, ensure then will perform an and , which can only strengthen the original. Hence the rule:
The last case -- retaining the original -- is frequent but by no means universal.
The Assertion Redeclaration rule applies to redeclarations . This terms covers not just redefinition but also effecting (the implementation, by a class, of a feature that it inherits deferred). The rules -- not just for assertions but also, as reviewed below, for typing -- are indeed the same in both cases. Without the Assertion Redeclaration rule, assertions on deferred features, such as those on extend , count and forth in "Deferred features and classes", page 57 , would be almost useless -- wishful thinking; the rule makes them binding on all effectings in descendants.
From the Assertion Redeclaration rule follows an interesting technique: abstract preconditions . What needs to be weakened for a precondition (or strengthened for a postcondition) is not the assertion's concrete semantics but its abstract specification as seen by the client. A descendant can change the implementation of that specification as it pleases, even to the effect of strengthening the concrete precondition, as long as the abstract form is kept or weakened. The precondition of procedure extend in the deferred class LIST provided an example. We wrote the routine (page 58 ) as
The precondition expresses that it is only possible to add an item to a list if the representation is not full. We may well consider -- in line with the Eiffel principle that whenever possible structures should be of unbounded capacity -- that LIST should by default make full always return false:
Now a class BOUNDED_LIST that implements bounded-size lists (inheriting, like the earlier ARRAYED_LIST , from both LIST and ARRAY ) may redefine full :
Procedure extend remains applicable as before; any client that used it properly with LIST can rely polymorphically on the FIXED_LIST implementation. The abstract precondition of extend has not changed, even though the concrete implementation of that precondition has in fact been strengthened.
Note that a class such as BOUNDED_LIST , the likes of which indeed appear in EiffelBase, is not a violation of the Eiffel advice to stay away from fixed-size structures. The corresponding structures are bounded, but the bounds are changeable. Although extend requires not full , another feature, called force in all applicable classes, will add an element at the appropriate position by resizing and reallocating the structure if necessary. Even arrays in Eiffel are not fixed-size, and have a procedure force with no precondition, accepting any index position.
The Assertion Redeclaration rule, together with the Invariant Accumulation rule, provides the right methodological perspective for understanding inheritance and the associated mechanisms. Defining a class as inheriting from another is a strong commitment; it means inheriting not only the features but the logical constraints. Redeclaring a routine is bound by a similar committment: to provide a new implementation (or, for an effecting, a first implementation) of a previously defined semantics, as expressed by the original contract. Usually you have a wide margin for choosing your implementation, since the contract only defines a range of possible behaviors (rather than just one behavior), but you must remain within that range. Otherwise you would be perverting the goals of redeclaration, using this mechanism as a sort of late-stage hacking to override bugs in ancestor classes.
It is not an error to inherit two deferred features from different parents under the same name, provided they have the same signature (number and types of arguments and result). In that case a process of feature join takes place: the features are merged into just one -- with their preconditions and postconditions, if any, respectively or-ed and and-ed.
More generally, it is permitted to have any number of deferred features and at most one effective feature that share the same name: the effective version, if present will effect all the others.
All this is not a violation of the Final Name rule (page 65 ), since the name clashes prohibited by the rule involve two different features having the same final name; here the result is just one feature, resulting from the join of all the inherited versions.
Sometimes we may want to join effective features inherited from different parents, assuming again the features have compatible signatures. One way is to redefine them all into a new version; then they again become one feature, with no name clash in the sense of the Final Name rule. But in other cases we may simply want one of the inherited implementations to take over the others. The solution is to revert to the preceding case by uneffecting the other features; uneffecting an inherited effective feature makes it deferred (this is the reverse of effecting, which turns an inherited deferred feature into an effective one). The syntax uses the undefine subclause:
g as f -- g was effective in A undefine f end -- f was effective in B -- C also has an effective feature f , which will serve as |
Again what counts, to determine if there is an invalid name clash, is the final name of the features. In this example to of the joined features were originally called f ; the one from A was called g , but in D it is renamed as f , so without the undefinition it would cause an invalid name clash.
Feature joining is the most common application of uneffecting. In some non-joining cases, however, it may be useful to forget the original implementation of a feature and let it start a new life devoid of any burden from the past.
Another Feature Adaptation subclause, export , makes it possible to change the export status of an inherited feature. By default -- covering the behavior desired in the vast majority of practical cases -- an inherited feature keeps its original export status (exported, secret, selectively exported). In some cases, however, this is not appropriate:
A feature may have played a purely implementation-oriented role in the parent, but become interesting to clients of the heir. Its status will change from secret to exported.
In implementation inheritance (for example ARRAYED_LIST inheriting from ARRAY ) an exported feature of the parent may not be suitable for direct use by clients of the heir. The change of status in this case is from exported to secret.
You can achieve either of these goals by writing
This gives a new export status to the features listed (under their final names since, as noted, export like all other subclauses comes after rename if present): they become exported to the classes listed. In most cases this list of classes, X , Y , …, consists of just ANY , to re-export a previously secret feature, or NONE , to hide a previously exported feature. It is also possible, in lieu of the feature list, to use the keyword all to apply the new status to all features inherited from the listed parent. Then there can be more than one class-feature list, as in
where any explicit listing of a feature, such as capacity , takes precedence over the export status specified for all . Here most features of ARRAY are secret in ARRAYED_LIST , because the clients should not permitted to manipulate array entries directly: they will manipulate them indirectly through list features such as extend and item , whose implementation relies on array_item and array_put . But ARRAY 's feature count remains useful, under the name capacity , to the clients of ARRAYED_LIST .
Thanks to inheritance, a concise class text may achieve a lot, relying on all the features inherited from direct and indirect ancestors.
This is part of the power of the object-oriented form of reuse, but can create a comprehension and documentation problem when the inheritance structures become deep: how does one understand such a class, either as client author or as maintainer? For clients, the Contract Form, entirely deduced from the class text, does not tell the full story about available features; and maintainers must look to proper ancestors for much of the relevant information.
These observations suggest ways to produce, from a class text, a version that is equivalent feature-wise and assertion-wise, but has no inheritance dependency. This is called the Flat Form of the class. It is a class text that has no inheritance clause and includes all the features of the class, immediate (declared in the class itself) as well as inherited. For the inherited features, the flat form must of course take account of all the feature adaptation mechanisms: renaming (each feature must appear under its final name), redefinition, effecting, uneffecting and export status change. For redeclared features, require else clauses are or-ed with the precursors' preconditions, and ensure then clauses are and-ed with precursors' postconditions. For invariants, all the ancestors' clauses are concatenated. As a result, the flat form yields a view of the class, its features and its assertions that conforms exactly to the view offered to clients and (except for polymorphic uses) heirs.
As with the Contract Form ( "The contract form of a class", page 44 ), producing the Flat Form is the responsibility of tools in the development environment. In EiffelStudio, you will just click the "Flat" icon.
The Contract Form of the Flat Form of a class is known as its F lat-Contract Form . It gives the complete interface specification, documenting all exported features and assertions -- immediate or inherited -- and hiding implementation aspects. It is the appropriate documentation for a class.
An inheritance mechanism, following from multiple inheritance, remains to be seen. Through multiple inheritance, a class can be a proper descendant of another through more than one path. This is called repeated inheritance and can be indirect, as in the following figure, or even direct, when a class D lists a class A twice in its inherit clause.
The figure's particular example is in fact often used by introductory presentations of multiple inheritance, which is a pedagogical mistake: simple multiple inheritance examples (such as INTEGER inheriting from NUMERIC and COMPARABLE , or COMPANY_PLANE from ASSET and PLANE ) should involve the combination of separate abstractions . Repeated inheritance is an advanced technique; although invaluable, it does not arise in elementary uses and requires a little more care.
In fact there is only one non-trivial issue in repeated inheritance: what does a feature of the repeated ancestor, such as change_address and computer_account , mean for the repeated descendant, here TEACHING_ASSISTANT ? (The example features chosen involve a routine and an attribute; the basic rules will be the same.)
There are two possibilities: sharing (the repeatedly inherited feature yields just one feature in the repeated descendant) and duplication (it yields two). Examination of various cases shows quickly that a fixed policy, or one that would apply to all the features of a class, would be inappropriate.
Feature change_address calls for sharing: as a teaching assistant, you may be both teacher and student, but you are just one person, with just one official domicile.
If there are separate accounts for students' course work and for faculty, you may need one of each kind, suggesting that computer_account calls for duplication.
The Eiffel rule enables, once again, the software developer to craft the resulting class so as to tune it to the exact requirements. Not surprisingly, it is based on names, in accordance with the Final Name rule (no in-class overloading):
So to tune the repeated descendant, feature by feature, for sharing and replication it suffices to use renaming.
Doing nothing will cause sharing, which is indeedthe desired policy in most cases (especially those cases of unintended repeated inheritance: making D inherit from A even though it also inherits from B , which you forgot is already a descendant of A ).
If you use renaming somewhere along the way, so that the final names are different, you will obtain two separate features. It does not matter where the renaming occurs; all that counts is whether in the common descendant, TEACHING_ASSISTANT in the last figure, the names are the same or different. So you can use renaming at that last stage to cause replication; but if the features have been renamed higher you can also use last-minute renaming to avoid replication, by bringing them back to a single name.
The Repeated Inheritance rule gives the desired flexibility to disambiguate the meaning of repeatedly inherited features. There remains a problem in case of redeclaration and polymorphism. Assume that somewhere along the inheritance paths one or both of two replicated versions of a feature f , such as computer_account in the example, has been redeclared; we need to define the effect of a call a . f ( a . computer_account in the example) if a is of the repeated ancestor type, here UNIVERSITY_PERSON , and has become attached as a result of polymorphism to an instance of the repeated descendant, here TEACHING_ASSISTANT . If one or more of the intermediate ancestors has redefined its version of the feature, the dynamically-bound call has two or more versions to choose from.
A select clause will resolve the ambiguity, as in
class TEACHING_ASSISTANT inherit computer_account as faculty_account |
We assume here that that no other renaming has occurred -- TEACHING_ASSISTANT takes care of the renaming to ensure replication -- but that one of the two parents has redefined computer_account , for example TEACHER to express the special privileges of faculty accounts. In such a case the rule is that one (and exactly one) of the two parent clauses in TEACHING_ASSISTANT must select the corresponding version. Note that no problem arises for an entity declared as
since the valid calls are of the form ta . faculty_account and ta . student_account , neither of them ambiguous; the call ta . computer_account would be invalid, since after the renamings class TEACHING_ASSISTANT has no feature of that name. The select only applies to a call
with up of type UNIVERSITY_PERSON , dynamically attached to an instance of TEACHING_ASSISTANT ; then the select resolves the ambiguity by causing the call to use the version from TEACHER .
So if you traverse a list computer_users : LIST [ UNIVERSITY_PERSON ] to print some information about the computer account of each list element, the account used for a teaching assistant is the faculty account, not the student account.
You may, if desired, redefine faculty_account in class TEACHING_ASSISTANT , using student_account if necessary, to take into consideration the existence of another account. But in all cases we need a precise disambiguation of what computer_account means for a TEACHING_ASSISTANT object known only through a UNIVERSITY_PERSON entity.
The select is only needed in case of replication. If the Repeated Inheritance rule would imply sharing, as with change_address, and one or both of the shared versions has been redeclared, the Final Name rule makes the class invalid, since it now has two different features with the same name. (This is only a problem if both versions are effective; if one or both are deferred there is no conflict but a mere case of feature joining as explained in "Join and uneffecting", page 70 .) The two possible solutions follow from the previous discussions:
If you do want sharing, one of the two versions must take precedence over the other. It suffices to undefine the other, and everything gets back to order. Alternatively, you can redefine both into a new version, which takes precedence over both.
If you want to keep both versions, switch from sharing to replication: rename one or both of the features so that they will have different names; then you must select one of them.
Eiffel's inheritance mechanism has an important application to extending the flexibility of the genericity mechanism. In a class SOME_CONTAINER [ G ] , as noted (section 7 ), the only operations available on entities of type G , the formal generic parameter, are those applicable to entities of all types. A generic class may, however, need to assume more about the generic parameter, as with a class SORTABLE_ARRAY [ G … ] which will have a procedure sort that needs, at some stage, to perform tests of the form
where item ( i ) and item ( j ) are of type G . But this requires the availability of a feature infix "<" in all types that may serve as actual generic parameters corresponding to G . Using the type SORTABLE_ARRAY [ INTEGER ] should be permitted, because INTEGER has such a feature; but not SORTABLE_ARRAY [ COMPLEX ] if there is no total order relation on COMPLEX .
To cover such cases, declare the class as
making it constrained generic . The symbol -> recalls the arrow of inheritance diagrams; what follows it is a type, known as the generic constraint. Such a declaration means that:
Within the class, you may apply the features of the generic constraint -- here the features of COMPARABLE : infix "<" , infix "<=" etc. -- to expressions of type G .
A generic derivation is only valid if the chosen actual generic parameter conforms to the constraint. Here you can use SORTABLE_ARRAY [ INTEGER ] since INTEGER inherits from COMPARABLE , but not SORTABLE_ARRAY [ COMPLEX ] if COMPLEX is not a descendant of COMPARABLE .
A class can have a mix of constrained and unconstrained generic parameters, as in the EiffelBase class HASH_TABLE [ G , H -> HASHABLE ] whose first parameter represents the types of objects stored in a hash table, the second representing the types of the keys used to store them, which must be HASHABLE . As these examples suggest, structural property classes such as COMPARABLE , NUMERIC and HASHABLE are the most common choice for generic constraints.
Unconstrained genericity, as in C [ G ] , is defined as equivalent to C [ G -> ANY ] .
The Type Conformance rule ( "Polymorphism", page 53 ) ensures type safety by requiring all assignments to be from a more specific source to a more general target.
Sometimes you can't be sure of the source object's type. This happens for example when the object comes from the outside -- a file, a database, a network. The persistence storage mechanism( "Deep operations and persistence", page 30 ) includes, along with the procedure store seen there, the reverse operation, a function retrieved which yields an object structure retrieved from a file or network, to which it was sent using store . But retrieved as declared in the corresponding class STORABLE of EiffelBase can only return the most general type, ANY ; it is not possible to know its exact type until execution time, since the corresponding objects are not under the control of the retrieving system, and might even have been corrupted by some external agent.
In such cases you cannot trust the declared type but must check it against the type of an actual run-time object. Eiffel introduces for this purpose the assignment attempt operation, written
with the following effect (only applicable if x is a writable entity of reference type):
If y is attached, at the time of the instruction's execution to an object whose type conforms to the type of x , perform a normal reference assignment.
Otherwise (if y is void, or attached to a non-conforming object), make x void.
Using this mechanism, a typical object structure retrieval will be of the form
"We did not get what we expected"
"Proceed with normal computation, which will typically involve |
As another application, assume we have a LIST [ ACCOUNT ] and class SAVINGS_ACCOUNT , a descendant of ACCOUNT , has a feature interest_rate which was not in ACCOUNT . We want to find the maximum interest rate for savings accounts in the list. Assignment attempt easily solves the problem:
Note that if there is no savings account at all in the list the assignment attempt will always yield void, so that the result of the function will be 0, the default initialization.
Assignment attempt is useful in the cases cited -- access to external objects beyond the software's own control, and access to specific properties in a polymorphic data structure. The form of the instruction precisely serves these purposes; not being a general type comparison, but only a verification of a specific expected type, it does not carry the risk of encouraging developers to revert to multi-branch instruction structures, for which Eiffel provides the far preferable alternative of polymorphic, dynamically-bound feature calls.
The final property of Eiffel inheritance involves the rules for adapting not only the implementation of inherited features (through redeclaration of either kind, redeclaration and redefinition, as seen so far) and their contracts (through the Assertion Redeclaration rule), but also their types. More general than type is the notion of a feature's signature , defined by the number of its arguments, their types, the indication of whether it has a result (that is to say, is a function or attribute rather than a procedure) and, if so, the type of the result.
In many cases the signature of a redeclared feature remains the same as the original's. But in some cases you may want to adapt it to the new class. Assume for example that class ACCOUNT has features
We introduce an heir BUSINESS_ACCOUNT of ACCOUNT to represent special business accounts, corresponding to class BUSINESS inheriting from HOLDER:
Clearly, we must redefine owner in class BUSINESS_ACCOUNT to yield a result of type BUSINESS ; the same signature redefinition must be applied to the argument of set_owner . This case is typical of the general scheme of signature redefinition: in a descendant, you may need to redefine both results and arguments to types conforming to the originals. This is reflected by a language rule:
The term "covariance" reflects the property that all types -- those of arguments and those of results -- vary together in the same direction as the inheritance structure.
If a feature such as set_owner has to be redefined for more than its signature -- to update its implementation or assertions -- the signature redefinition will be explicit. For example set_owner could do more for business owners than it does for ordinary owners. Then the redefinition will be of the form
In other cases, however, the body will be exactly the same as in the precursor. Then explicit redefinition would be tedious, implying much text duplication. The mechanism of anchored redeclaration solves this problem. The original declaration of set_owner in ACCOUNT should be of the form
A like anchor type, known as an anchored type, may appear in any context in which anchor has a well-defined type; anchor can be an attribute or function of the enclosing class, or an argument of the enclosing routine. Then, assuming T is the type of anchor , the type like anchor means the following:
In the class in which it appears, like anchor means the same as T . Ffor example, in set_owner above, the declaration of h has the same effect as if h had been declared of type HOLDER , the type of the anchor owner in class ACCOUNT .
The difference comes in proper descendants: if a type redefinition changes the type of anchor , any entity declared like anchor will be considered to have been redefined too.
This means that anchored declaration are a form of of implicit covariant redeclaration.
In the example, class BUSINESS_ACCOUNT only needs to redefine the type of owner (to BUSINESS ). It doesn't have to redefine set_owner except if it needs to change its implementation or assertions.
It is possible to use Current as anchor; the declaration like Current denotes a type based on the current class (with the same generic parameters if any). This is in fact a common case; we saw in "Structural property classes", page 62 , that it applies in class COMPARABLE to features such as
since we only want to compare two comparable elements of compatible types -- but not, for example, integer and strings, even if both types conform to COMPARABLE . (A "balancing rule" makes it possible, however, to mix the various arithmetic types, consistently with mathematical traditions, in arithmetic expressions such as 3 + 45.82 or boolean expressions such as 3 < 45.82 .)
Similarly, class ANY declares procedure copy as
with the argument anchored to the current object. Function clone , for its part, has signature clone ( other : ANY ): like other , with both argument and result anchored to the argument, so that for any x the type of clone ( x ) is the same as the type of x .
A final, more application-oriented example of anchoring to Current is the feature merge posited in an earlier example (page 33 ) with the signature merge ( other : ACCOUNT ). By using instead merge ( other : like Current ) we can ensure that in any descendant class -- BUSINESS_ACCOUNT , SAVINGS_ACCOUNT , MINOR_ACCOUNT … -- an account will only be mergeable with another of a compatible type.
Covariance makes static type checking more delicate; mechanisms of "system validity" and "catcalls" address the problem, discussed in detail in the book Object-Oriented Software Construction (see the bibliography).
Eiffel Home Page (Web) -- Getting started with Eiffel (local)
Copyright Interactive Software Engineering, 2001