19.7 Inheritance

19.7.1 Why Inherit

All that has been said thus far is merely syntactical sugar for things that could have been done other ways in the base syntax, without involving OOM-2 at all. Indeed, for many data types, static modules implement ADTs quite nicely, and for most of the rest, Generic templates pick up the remaining slack. However, the real power of using object methods lies in the ability to create objects that are based on pre-existing ones, with perhaps a few changes. This allows the code already written for use in one object class to be reused in another object class, or, perhaps more accurately, it allows the functionality desired for some new object to be handed off to an existing object. With either way of looking at it, the wheel does not have to be re-invented quite so often.

The ability to take an existing object and make it the basis of a new one is called inheritance.

A class that inherits from another one is called a derived class or a child class or a subclass.

A class that has one or more children is called a parent class or an ancestor class or a superclass.

Object oriented software also differs from generic approaches in that much of what is done with objects is dynamic, whereas generic templates are simply means of generating new and specialized static modules. There are various strategies for inheritance among the common object oriented languages. In some, a class can inherit from several other classes, incorporating all their features into itself (multiple inheritance). In others, a class is permitted to inherit only from one other class--though it in turn could inherit from another one--(single inheritance.)

19.7.2 Inheritance in OOM-2

The basic model for OOM-2 is that of single inheritance. One class can say that it is inheriting another class, and this acts in a sense as an import does into a module scope. All the components of a parent class are available inside the scope of the subclass, not just the ones that have been revealed for clients. This allows the subclass declaration to add additional attributes or methods of its own while having full access to those of the original class. It can reveal new components that it defines, but it cannot reveal. An exception is that hidden components (found only in the implementation of a separate library module, not in its definition) are not accessible to subclasses. Apart from this, each class controls the visibility of its own components.

However, a chain of inheritances may be built in OOM-2, wherein a class inherits from a second class which in turn inherits from an third class, and so on, with each new class making changes to the previous component structure.

WARNING: In OOM-2 a traced class can inherit only from a traced class, and an untraced class can inherit only from an untraced class.

The reader should be able to see the logic of the above restriction. It would make no sense to derive a class the objects of which the garbage collector is supposed to trace from a class whose objects it cannot trace, and vice-versa.

Starting with the class Date defined in the previous section, we can create a new subclass with additional properties in the following manner, but this time in a program rather than in a library:

MODULE DemoDates1;
(* simple demonstration of inheritance in OOM-2
    by R. Sutcliffe; revised: 1998 09 22 *)
IMPORT Dates;
FROM SWholeIO IMPORT
  WriteCard;
FROM STextIO IMPORT
  WriteString, WriteChar;

TRACED CLASS Dates1;
  INHERIT Dates.Date;
  REVEAL WriteUSDate;

  PROCEDURE WriteUSDate;
  VAR
    monthStr : ARRAY [0..8] OF CHAR;
  BEGIN
    IF month = 1
      THEN
        monthStr := "January"
      ELSIF month = 2 THEN
        monthStr := "February"
      ELSIF month = 3 THEN
        monthStr := "March"
      ELSIF month = 4 THEN
        monthStr := "April"
      ELSIF month = 5 THEN
        monthStr := "May"
      ELSIF month = 6 THEN
        monthStr := "June"
      ELSIF month = 7 THEN
        monthStr := "July"
      ELSIF month = 8 THEN
        monthStr := "August"
      ELSIF month = 9 THEN
        monthStr := "September"
      ELSIF month = 10 THEN
        monthStr := "October"
      ELSIF month = 11 THEN
        monthStr := "November"
      ELSIF month = 12 THEN
        monthStr := "December"
      ELSE
        monthStr := "xxxxxxx"
      END;
    WriteString (monthStr);
    WriteCard (day, 0);
    WriteChar (",");
    WriteCard (year, 0);
  END WriteUSDate;
END Dates1;

VAR
  aDate : Dates1;
BEGIN
  CREATE (aDate);
  aDate.SetDate (1998, 11, 5);
  aDate.WriteUSDate;
END DemoDates1.

This program has the output:

November 5, 1998

If the classes have class bodies (constructors), the order that these are run is from root to leaf, that is from the eldest parent through its child, and so on down to the last subclass.

If one is dealing with a chain of untraced classes, and these each have a destructor or FINALLY clause (recall that this is impossible with traced classes) then these are executed in reverse order, that is from child class to parent to grandparent and so on up to the top superclass. Care must be taken with the use of FINALLY clauses in this respect; this order might well be different from the order of finalization of the modules in which these classes are defined, and if the module finalizations could interfere with the object finalizations. Such complications are a good reason to use traced classes; then the programmer does not have to worry about object finalization.

19.7.3 Assignment Compatibility between Classes and Subclasses

In OOM-2, a subclass BClass of a superclass AClass is also a subtype of AClass. This means that if one has, for instance:

VAR
   superObject : AClass;
   subObject : BClass;

then subObject is assignment compatible with superObject, but not vice versa, so that

subObject := superObject;

is incorrect, but

superObject := subObject;

is correct.

What happens here (in the latter case) is that the variable superObject, which has been statically declared to be of (class) type AClass, has been dynamically been assigned an object of (class) type BClass. That is to say, the dynamic type of an object referenced by an identifier may not be the same as its static declaration, it may dynamically become that of a subtype.

It may at time be necessary to check on the dynamic type status of a class variable, and this can be done with the function ISMEMBER, which returns a boolean according to the following rule:

ISMEMBER (param1, param2);

allows either parameter to be a class name or an object reference. ISMEMBER returns TRUE if objects of the class type of the first parameter are assignment compatible (subclass or same class) to objects of the type of the second parameter and FALSE otherwise. That is, ISMEMBER can be invoked as

ISMEMBER (objectRef, Classname2);

and returns TRUE if objectRef is a member of (a subclass of) Classname2. If the first parameter is a class and the second an object reference, ISMEMBER returns TRUE if objects of the class are assignment compatible with the given object. Moreover,

ISMEMBER (ClassName1, Classname2);

returns true if objects of ClassName1 are assignment compatible (subclass or same class) to objects of the ClassName2 Class, and false otherwise (that is, if the class of the first parameter is equal to or is a subclass of the second parameter).

As another fine point on static vs dynamic semantics, the note at the end of section 19.7.3 is to be taken in the sense that the constructor chain is evaluated in static order, but the destructor chain (where present) is evaluated dynamically.

19.7.4 Overriding Methods in Subclasses

A class can do more than just decide to inherit the components of a superclass. It can also replace some of the method components (though not the attribute ones) with different ones, provided they have the same interface. That is, a method of the superclass can be overridden in a subclass by a new method provided it has the same definition (name and parameters) as the original. The new method applies to objects of the subclass, but not to those of the superclass. It has the same syntax as the original method but different semantics. All this is accomplished merely by placing the reserved word OVERRIDE in front of the new method.

The subclass can have attribute components added to the original list, but none of the attribute components can be overridden, because the only point to doing that would be to alter their type, and then the interface would have changed, and the new class would not properly be a child class at all.

Here is a simple example to illustrate this syntax and semantics. The first class shown implements adding accumulators. These are commonly used in programs to keep running sums and are initialized to zero. The class exports Clear to reset the accumulator, Accumulate to add to the register variable (which is kept private) and Display to print the result. We could just abandon one accumulator after using it a while and create a new one, not bothering with Clear, but this seems unnecessarily messy.

The reader should observe that it is considered to be good taste to have an object know how to display itself (if this is needed) rather than allowing outside information to be leaked so that it can be displayed external to the object. That is, one keeps data hidden and provides procedures to manipulate it where possible.

The second class has the same interface, but accumulates a running product instead. It can use the same Display, and reveals the same components, but overrides both Clear and Accumulate for different semantics, because a multiplication register has to be initialized to zero, and of course, has to multiply each new item rather than add it.

MODULE OverrideDemo;
(* demonstration of inheritance and overriding methods
  R. Sutcliffe 1998 09 21 *)
  
FROM SLongIO IMPORT
  WriteReal;
FROM STextIO IMPORT
  WriteLn;

(* first class implements an adding accumulator *)
TRACED CLASS Accumulator;
  REVEAL Clear, Accumulate, Display;
  VAR
    register : LONGREAL;
  
  PROCEDURE Clear;
  BEGIN
    register := 0.0;
  END Clear;
  
  PROCEDURE Accumulate (new : LONGREAL);
  BEGIN
    register := register + new;
  END Accumulate;
  
  PROCEDURE Display ;
  BEGIN
    WriteReal (register, 15);
  END Display;

BEGIN (* class body *)
  Clear;
END Accumulator;
    
VAR
  acc : Accumulator;

(* second class does the same thing but with a multiplying version *)
TRACED CLASS AccumulatorM;
  INHERIT Accumulator;  (* same interface *)  
  OVERRIDE PROCEDURE Clear;
  BEGIN
    register := 1.0; (* if cleared to zero, multiplications wouldn't do much. *)
  END Clear;
    
  OVERRIDE PROCEDURE Accumulate (new : LONGREAL);
  BEGIN
    register := register * new;
  END Accumulate;
      
BEGIN
  Clear; (* uses this class' clear *)
END AccumulatorM;
    
VAR
  accM : AccumulatorM;
        
BEGIN
  CREATE (acc); (* set up an adder *)
  acc.Accumulate (9.8); (* and exercise it *)
  acc.Accumulate (6.7);
  acc.Accumulate (11.35);
  acc.Display;
  WriteLn;
  
  CREATE (accM); (* now do a multiplier *)
  accM.Accumulate (9.8);
  accM.Accumulate (6.7);
  accM.Accumulate (11.35);
  accM.Display; (* uses the original; not overriden *)
  WriteLn;

END OverrideDemo.

The output from this module is:

27.850000000000
745.24100000000

Observe that even within the subclass, a reference to Clear is to the overridden method, and not to the one in the superclass. In this case, there were no additions needed to the list of things to be made public, so the subclass did not need a REVEAL clause at all.

Outside the class, client software references are to the dynamic class of the object. Thus, id one were to write

accM := acc;

and thereby change the dynamic class of acc, it would become an adder rather than a multiplier, because the methods would be chosen according to the dynamic class.

It is important to realize that overriding a method does not create a new component or do anything with scope. Rather, it replaces the implementation of the method with a new one and retains the interface as it was. Thus, the overriding method must have the same procedure type as the original one. If it is a regular procedure, the parameters must be the same in number, position, and type; if a function procedure, the return type must also be identical. This also means that an inherited identifier is in the scope of the subclass; it cannot simply be re-used for something else. Overriding is changing the implementation; it is not replacing the name with a new one that happens to be spelled the same way.

In the event that the class inherited from had a definition and declaration in a separate library module, and one plans to override methods of a superclass, this must be signalled in the definition with the word OVERRIDE and then implemented accordingly. Moreover, where such a class definition has been made, inheritance must be specified in the definition only and not in the declaration. The INHERIT clause was allowed in the previous example within a declaration only because there was no definition already in existence.

So, for instance, if one wanted to define and declare a new separate Date class, this time overriding the method for date display rather than adding a new one as previously, the modules would be written as follows:

DEFINITION MODULE Dates2;
(* Simple class override definition
by R. Sutcliffe 1998 09 21 *)
FROM Dates IMPORT
  Date;

TRACED CLASS Date2;
  INHERIT Date;
  OVERRIDE PROCEDURE WriteDate;
END Date2;

 END Dates2.

Observe the import of the superclass and its inheritance here, followed by the flagging of an override for the one method. Everything else has been left as it was. In the implementation below, no inheritance clause is needed (the one in the definition suffices) and all that remains is to implement the overridden method.

IMPLEMENTATION MODULE Dates2;
(* Simple class override implementation 
by R. Sutcliffe 1998 09 21 *)

FROM SWholeIO IMPORT
  WriteCard;
  
TRACED CLASS Date2;   
  OVERRIDE PROCEDURE WriteDate;
  BEGIN
    WriteCard (day, 0);
    WriteCard (month, 0);
    WriteCard (year, 0);
  END WriteDate;   
END Date2;

END Dates2.

Of course, once having flagged a method for overriding in the definition, it must actually be done in the implementation or the compiler will generate an error.

19.7.5 Class and Object References

There are several situations worth noting where code has to use object or class references in somewhat unusual ways.

Accessing an Overridden Method

Sometimes in the declaration of a class it may be necessary to refer to an overridden method from a superclass. Because it has been overridden, the original name is not available for use in the subclass. If this is the case, it can be referred to by the syntax

superclassName.originalMethod

even if the superclass referenced is several levels up the inheritance chain. By this means, all the methods up the chain are available for use in a subclass, if necessary. Such methods are not directly available outside the subclass, however, unless it defines and reveals a new method that simply calls the old one under a new name.

TRACED CLASS sub;
  INHERIT oldClass;
   REVEAL newMethod;
   OVERRIDE PROCEDURE oldMethod;
   ....
   PROCEDURE newMethod;
   BEGIN
     oldClass.oldMethod;
   END;
 END sub;

However, in such a case, it seems to make more sense to keep the old method under its original name and define a new one with a different name. This type of reference is more likely to be used if the old method is incorporated into and made a part of the new one, or used in the initialization body.

Using a Class Name as a Qualifier

As indicated in section 19.3.3, one cannot refer to a method of a class by using its class name as qualifying identifier. After all, without the name of an object to qualify them, such a method has nothing to act upon. One also may not refer to a variable declared in a class in such a manner. However, constants and types declared in a class can be referred to outside the class in this manner, providing they have been revealed. Indeed, this is the only way to refer outside the class to a type declared in a class.

TRACED CLASS Classy;
  REVEAL myType,   (* can refer to outside as Classy.myType *)
          myVar,    (* qualify outside only by an object name of this class *)
          myConst,  (* OK to use outside as Classy.myConst, not in constant expression *)
          myMethod; (* qualify outside only by an object name of this class *)
   TYPE
     myType : ARRAY [1..2] OF CARDINAL;
   VAR
     myVar : myType;
   CONST
     pi = 3.14159;
   PROCEDURE myMethod;
     some body
   END myMethod;
 END Classy;

SELF--The Hidden Parameter

On occasion, it may be necessary for an object to refer to itself somewhere in its declaration. This could come about, for instance, if one of its methods took as a parameter an item whose type is that of the object. In such cases, there is always a reference available to the object using the identifier SELF, and this can in turn of course qualify any component if desired.

 TRACED CLASS Naval;
   VAR
     ok : BOOLEAN;
   PROCEDURE Gaze (at : Naval);
   some code
   END Gaze;

   PROCEDURE check (VAR allRight : BOOLEAN);
      some code
   END Gaze;

 BEGIN (* initialization body or another method body *)
   Gaze (SELF);
   Check (SELF.ok)
 END Naval;

Inside the various methods, and the initialization body, SELF is available as a kind of hidden parameter, so that every method "knows" what object invoked it and has access to its other components.

It should be obvious that SELF cannot be re-assigned; after all, to do so within itself would create a logical tangle. It is called an immutable entity, just as are any class variables marked as READONLY.

Yet another time that SELF may be useful is if it become necessary to assign the object reference to some variable defined globally to the class scope.

TRACED CLASS SomeClass;
  PROCEDURE AMethod;
   BEGIN
     IF condition
       THEN
         globalVar := SELF;
       END;
     END AMethod;
 END SomeClass;

Circular Definitions and Declarations

Suppose that two class definitions or declarations make references to each other, in the same manner that two procedures may invoke each other. Perhaps each declares an object variable of the other class type. In this case, neither may come first if there is a strict "declare it before use" rule, so OOM-2 allows the use of FORWARD in such a context, in a manner similar to the way it is used in mutually recursive procedures.

CLASS EventHandling; FORWARD
CLASS MyWindow;
  VAR
    event : EventHandling;
  ...
END MyWindow;
CLASS EventHandling;
  VAR
    theWindow : MyWindow;
     ...
END EventHandling;

19.7.6 Why Single Inheritance?

Some OO languages use multiple inheritance. That is, a class is permitted to inherit from more than superclass. The reader who immediately thinks of a score of projects that such a facility would enable ought to consider the following example. Suppose one defines four classes Parent, Daughter, Son, and Grandkid. Class Parent defines a component Pathological. Daughter and Son both override Pathological and define their own version. Grandkid inherits from both Daughter and Son, and does not override Pathological. In this admittedly incestuous tangle, which version of Pathological is a component of Grandkid?

Because this problem cannot be resolved without introducing some special magical rule (such as: all versions of a contested method have to be qualified by a class name), many languages, including OOM-2 decline to get involved in such problems by allowing only single inheritance in the first place.


Contents