One class is derived from another using the
OF keyword in the definition of the derived class
OBJECT, in a similar way that
OF is used with methods.
For instance, the following code shows how to define the class
d to be derived from class
b is then said to be inherited by the class
OBJECT b b_data ENDOBJECT OBJECT d OF b extra_d_data ENDOBJECT
d have been chosen to be somewhat suggestive, since the class which is inherited (i.e.,
b) is known as the base class, whilst the inheriting class (i.e.,
d) is known as the derived class.
The definition of
d is the same as the following definition of
duff, except for one major difference: with the above derivation the methods of
b are also inherited by
d and they become methods of class
The definition of
duff relates it in no way to
b, except at best accidentally (since any changes to
b do not affect
duff, whereas they would affect
OBJECT duff b_data extra_d_data ENDOBJECT
One property of this derivation applies to the data records built by
OBJECT as well as the OOP classes.
The data records of type
duff may be used wherever a data record of type
b were required (e.g., the argument to some procedure), and they are, in fact, indistinguishable from records of type
Although, if the definition of
b were changed (e.g., by changing the name of the
b_data element) then data records of type
duff would not be usable in this way, but those of type
d still would.
Therefore, it is wise to use inheritance to show the relationships between classes or data of
The following example shows how procedure
print_b_data can validly be called in several ways, given the definitions of
PROC print_b_data(p:PTR TO b) WriteF('b_data = \d\n', p.b_data) ENDPROC PROC main() DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff NEW p_b, p_d, p_duff p_b.b_data:=11 p_d.b_data:=-3 p_duff.b_data:=27 WriteF('Printing p_b: ') print_b_data(p_b) WriteF('Printing p_d: ') print_b_data(p_d) WriteF('Printing p_duff: ') print_b_data(p_duff) ENDPROC
So far, no methods have been defined for
b, which means that it is just an
print_b_data suggests a useful method of
b, which will be called
PROC print() OF b WriteF('b_data = \d\n', self.b_data) ENDPROC
This definition would also define a
d is derived from
b and it inherits all the methods of
duff would, of course, still be just an
OBJECT type, although it could have a similar
b has any methods defined for it (i.e., if it is a class) then data records of type
duff cannot be used as if they were objects of the class
b, and it is not safe to try!
In this case, only objects of derived class
d can be used in this manner.
b is a class then
d is a class, due to inheritance.)
PROC main() DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff NEW p_b, p_d, p_duff p_b.b_data:=11 p_d.b_data:=-3; p_d.extra_d_data:=3 p_duff.b_data:=7; p_duff.extra_d_data:=-7 WriteF('Printing p_b: ') /* b explicitly has print method */ p_b.print() WriteF('Printing p_d: ') /* d inherits print method from b */ p_d.print() WriteF('No print method for p_duff\n') /* Do not try to print p_duff in this way */ /* p_duff.print() */ ENDPROC
d only prints the
b_data element (since it is really a method of
b, so cannot access the extra data added in
However, any inherited method can be overridden by defining it again, this time for the derived class.
PROC print() OF d WriteF('extra_d_data = \d, ', self.extra_d_data) WriteF('b_data = \d\n', self.b_data) ENDPROC
With this extra definition, the same
main procedure above would now print all the data of
d, but only the
b_data element of
This is because the new definition of
d (and classes derived from
Inherited methods are often overridden just to add extra functionality, as in the case above where we wanted the extra data to be printed as well as the data derived from
For this purpose, the
SUPER operator can be used on a method call to force the base class method to be used, where normally the derived class method would be used.
So, the definition of the
d could call the
PROC print() OF d WriteF('extra_d_data = \d, ', self.extra_d_data) SUPER self.print() ENDPROC
Be careful, though, because without the
SUPER operator this would involve a recursive call to the
d, rather than a call to the base class method.
Just as data records of type
d can be used wherever data records of type
b were required, objects of class
d can used in place of objects of class
The following procedure prints a message and the object data, using the
(Of course, only the methods named by class
b can be used in such a procedure, since the pointer
p is of type
PTR TO b.)
PROC msg_print(msg, p:PTR TO b) WriteF('Printing \s: ', msg) p.print() ENDPROC PROC main() DEF p_b:PTR TO b, p_d:PTR TO d NEW p_b, p_d p_b.b_data:=11 p_d.b_data:=-3; p_d.extra_d_data:=3 msg_print('p_b', p_b) msg_print('p_d', p_d) ENDPROC
You can't use
duff now, since it is not a class and
b is, and
msg_print expects a pointer to class
The only other objects that can be passed to
msg_print are objects from classes derived from
b, and this is why
p_d can be printed using
If you collect together the code and run the example you will see that the call to
msg_print uses the overridden
msg_print is called with
p_d as a parameter.
That is, the correct method is called even though the pointer
p is not of type
PTR TO d.
This is called polymorphism: different implementations of
Here's what should be printed:
Printing p_b: b_data = 11 Printing p_d: extra_d_data = 3, b_data = -3
Inheritance is not limited to a single layer: you can derive other classes from
b, you can derive classes from
d, and so on.
For instance, if class
e is derived from class
d then it would inherit all the data of
d and all the methods of
This means that
e would inherit the richer version of
e would have two base classes,
d, but would be derived directly from
d (and indirectly from
d would therefore be known as the super class of
e is derived directly from
(The super class of
d is its only base class,
SUPER operator is actually used to call the methods in the super class.
In this example, the
SUPER operator can be used in the methods of
e to call methods of
The binary tree implementation above (see 16.3 Binary Trees) suggests a good example for a class hierarchy (a collection of classes related by inheritance).
A basic tree structure can be encapsulated in a base class definition, and then specific kinds of tree (with different data at the nodes) can be derived from this.
In fact, the base class
tree defined below is only useful for inheriting, since a tree is pretty useless without some data attached to the nodes.
Since it is very likely that objects of class
tree will never be useful (but objects of classes derived from
tree would be), the
tree class is called an abstract class.
OBJECT tree left:PTR TO tree, right:PTR TO tree ENDOBJECT PROC nodes() OF tree DEF tot=1 IF self.left THEN tot:=tot+self.left.nodes() IF self.right THEN tot:=tot+self.right.nodes() ENDPROC tot PROC leaves(show=FALSE) OF tree DEF tot=0 IF self.left tot:=tot+self.left.leaves(show) ENDIF IF self.right tot:=tot+self.right.leaves(show) ELSEIF self.left=NIL IF show THEN self.print_node() tot++ ENDIF ENDPROC tot PROC print_node() OF tree WriteF('<NULL> ') ENDPROC PROC print() OF tree IF self.left THEN self.left.print() self.print_node() IF self.right THEN self.right.print() ENDPROC
leaves methods return the number of nodes and leaves of the tree, respectively, with the
leaves method taking a flag to specify whether the leaves should also be printed.
These methods should never need overriding in a class derived from
tree, and neither should
print_node method probably should be overridden, as is the case in the integer tree defined below.
OBJECT integer_tree OF tree int ENDOBJECT PROC create(i) OF integer_tree self.int:=i ENDPROC PROC add(i) OF integer_tree DEF p:PTR TO integer_tree IF i < self.int IF self.left p:=self.left p.add(i) ELSE self.left:=NEW p.create(i) ENDIF ELSEIF i > self.int IF self.right p:=self.right p.add(i) ELSE self.right:=NEW p.create(i) ENDIF ENDIF ENDPROC PROC print_node() OF integer_tree WriteF('\d ', self.int) ENDPROC
This is a nice example of polymorphism at work: we can implement a
tree which works with integers simply by defining the appropriate methods.
leaves method (of the
tree class) will then automatically call the
integer_tree version of
print_node whenever we pass it an
The definitions of
integer_tree can even be in different modules (see 17.5 Data-Hiding in E), and, using these OOP techniques, the module containing
tree would not need to be recompiled even if a class like
integer_tree is added or changed.
This shows why OOP is good for code-reuse and extensibility: with traditional programming techniques we would have to adapt the binary tree functions to account for integers, and again for each new datatype.
Notice that the recursive use of the new method
add must be called via an auxiliary pointer,
p, of the derived class.
This is because the
right elements of
tree are pointers to
tree objects and
add is not a method of
tree (the compiler would reject the code as a syntax error if you tried to directly access
add under these circumstances).
Of course, if the
tree class had an
add method there would not be this problem, but what would the code be for such a method?
add method does not really make sense for
tree, but if almost all classes derived from
tree are going to need such a method it might be nice to include it in the
tree base class.
This is the purpose of abstract methods.
An abstract method is one which exists in a base class solely so that it can be overridden in some derived class.
Normally, such methods have no sensible definition in the base class, so there is a special keyword,
EMPTY, which can be used to define them.
For example, the
add method in
tree would be defined as below.
PROC add(x) OF tree IS EMPTY
With this definition, the code for the
add method for the
integer_tree class could be simplified.
(The auxiliary pointer,
p, is still needed for use with
NEW, since an expression like
self.left is not a pointer variable.)
PROC add(i) OF integer_tree DEF p:PTR TO integer_tree IF i < self.int IF self.left self.left.add(i) ELSE self.left:=NEW p.create(i) ENDIF ELSEIF i > self.int IF self.right self.right.add(i) ELSE self.right:=NEW p.create(i) ENDIF ENDIF ENDPROC
This, however, is not the best example of an abstract method, since the
add method in every class derived from
tree must now take a single
LONG value as an parameter, in order to be compatible.
In general, though, a class representing a tree with node data of type t would really want an
add method to take a single parameter of type t.
The fact that a
LONG value can represent a pointer to any type is helpful, here.
This means that the definition of
add may not be so limiting, after all.
print_node method is much more obviously suited to being an abstract method.
The above definition prints something silly, because at that point we didn't know about abstract methods and we needed the method to be defined in the base class.
A much better definition would make
PROC print_node() OF tree IS EMPTY
It is quite safe to call these abstract methods, even for
tree class objects.
If a method is still abstract in any class (i.e., it has not been overridden), then calling it on objects of that class has the same effect as calling a function which just returns zero (i.e., it does very little!).
integer_tree class could be used like this:
PROC main() DEF t:PTR TO integer_tree NEW t.create(10) t.add(-10) t.add(3) t.add(5) t.add(-1) t.add(1) WriteF('t has \d nodes, with \d leaves: ', t.nodes(), t.leaves()) t.leaves(TRUE) WriteF('\n') WriteF('Contents of t: ') t.print() WriteF('\n') END t ENDPROC
Go to the Next or Previous section, the Detailed Contents, or the Amiga E Encyclopedia.