Inheritance and polymorphism: Difference between revisions

From m204wiki
Jump to navigation Jump to search
mNo edit summary
Line 575: Line 575:
   
   
==Inheritance and constructors==
==Inheritance and constructors==
An extension class might or might not have any <var>[[Janus SOAP User Language Interface#constructor|constructors]]</var>.
An extension class might or might not have any <var>[[Object oriented programming in SOUL#constructor|constructors]]</var>.
If an extension class has no constructors, the default (<var>[[Object variables#Using New or other Constructors|New]]</var>) constructors for the base classes are called in sequence with no parameters.
If an extension class has no constructors, the default (<var>[[Object variables#Using New or other Constructors|New]]</var>) constructors for the base classes are called in sequence with no parameters.
However, if any of the base class constructors require parameters or
However, if any of the base class constructors require parameters or

Revision as of 22:02, 22 October 2013

Background

Inheritance and polymorphism are two closely related but subtly different concepts that are often considered cornerstones of object-oriented programming. Inheritance allows a programmer to create an object class that has all the characteristics of another object class (or that inherits all that classes attributes) and that has additional characteristics or attributes. Such a class is often called an extension class, because it extends the capabilities of what is often called a base class.

Polymorphism allows objects of different classes to be used interchangeably. Since the classes that can be used interchangeably are almost always a base class and one of its extension classes, or are extension classes of the same base class, polymorphism is strongly related to inheritance.

To create an extension class of a base class, simply include the Extends phrase in the first Class statement for the class:

class ropeList extends stringList ... end class

As this example illustrates, it is possible to extend many system classes as well as user-defined classes. There are exceptions, however:

  • It is not possible to extend the following types of system classes:
  • It is possible to extend a file class in a specific context. For example, the following Class statement extends the Recordset class for file FOOBAR:

    class fooRecords extends recordset in file foobar

  • It is not possible to generically extend any file classes. For example, the following is not allowed:

    class myRecordset extends recordset

When one class extends another class, members of the base class can be accessed via variables of the extension class. For example, if RopeList extends the system Stringlist class, and %rope is of the RopeList class, any Stringlist class member can be accessed by preceding the member name with the Stringlist class name in parentheses:

%rope:(stringList)add('Added item')

If the creator of the RopeList class wants users of the class to be able to use Stringlist class members without specifying the class name on each invocation, the Inherit keyword can be specified after the class name in the Extends clause on the Class statement:

class ropeList extends stringList inherit ... end class

If Inherit is specified on the Class statement, the Add method in the previous example can be accessed without specifying the Stringlist class:

%rope:add('Added item')

Since the main reason to use extension classes is to add functionality to the base class, you may define Public, Private, Public Shared, and Private Shared members in an extension class, and you may access these members like any other member of any other class:

class ropeList extends stringList inherit public property first is longstring property last is longstring subroutine print variable name is string len 32 end public ... end class

A member of an extension class may have the same name as a member of a base class. In the example above, the RopeList class has a Print subroutine that has the same name as a Stringlist class method. In such a case, an unqualified reference to the member such as the following refers to the member in the extension class:

%rope:print

This is called member hiding, because a member in the base class is hidden by a member in the extension class. In such cases, however, it is always possible to access the base class member by indicating the class name in parentheses, before the member name:

%rope:(Stringlist)print

This accessibility means someone writing an extension class should never assume that a base class member with the same name as an extension class member will never be accessed.

Another way to access a base class member instead of using an extension class member with the same name is to use polymorphism. With polymorphism, one can always assign an extension class variable to a base class variable:

%rope is object ropeList %string is object stringList ... %rope = new %rope:name = 'Percy' %string = %rope %string:print

This assignment is possible because an object of an extension class is also considered to be an object of the base class. Many object-oriented programming textbooks use animals to illustrate this point: if an object is of class Otter, it is also of class Mammal, so there is nothing wrong with assigning it to a Mammal object variable. Once assigned to a base class variable, all member name references access the base class member of that name, even if the extension class has an eponymous member name. If this were not so, someone referencing a member via a base class variable might access a member of a class they did not even know about. Although there are class members that can be overridden by extension classes, these members must be declared as such so that users of the class are aware this might happen. These types of members are discussed in "Dynamic dispatch".

In addition to allowing unqualified access to base class members (via the Inherit keyword in the Extends clause of the Class statement), it is possible to selectively allow unqualified access to individual base class members. In the following class, any reference to the Print member via a RopeList class variable accesses the Stringlist member of that name:

class ropeList extends stringList public property first is longstring property last is longstring inherit print from stringList variable name is string len 32 end public ... end class

The Inherit declaration can also be used to map a base class method to a different extension class name:

class ropeList extends stringList inherit public property first is longstring property last is longstring subroutine print inherit display from stringList print variable name is string len 32 end public ... end class

In this example, a reference to the Display member of the RopeList class actually accesses the Print method of the Stringlist base class. This can be useful if, as in this example, there is a conflict between a base class member name and an extension class member name.

While it is best to use unique member names between extension and base classes, sometimes naming conflicts can happen by accident when a new base class member is added that has the same name as an existing extension class member. The programmer adding the member might not be aware that the member name is already in use in an extension class.

Although adding a member with a name already in use produces a naming issue for applications wanting to use the new base class member name, it does not affect existing applications:

  • References to the extension class member name via extension class variables continue to reference the extension class member (member hiding).
  • Existing applications do not have references to the member name via base class variables or explicit base class specification, because the member name did not exist in the base class.

Multiple inheritance

SOUL allows a class to extend more than a single class. This seems reasonable, as many objects in the real world can be thought of as extensions to more than one base class. For example, one might think of a dog as an extension to a Mammals class and to a Pets class. Or a person's car can be thought of as an extension to an Automobile class and to a Property class.

To extend multiple classes with SOUL, you separate the extended classes with the And keyword in the Class statement Extends clause:

class pet extends animal and property

The Inherit keyword can follow any or all of the extended class names:

class pet extends animal inherit and property

or

class pet extends animal inherit and property inherit

In general, having Inherit keywords on more than one extended class is not recommended, unless those classes are maintained to avoid name collisions. A name collision is generally treated as a compilation error. In the above example, if the Animal and Property classes both had members called Weight, the class declaration would result in a compilation error.

You can prevent name collision errors by specifying the IgnoreDuplicates keyword after the Inherit keyword for a base class for which duplicates are to be ignored:

class pet extends animal inherit and property inherit ignoreDuplicates

Duplicate member names are ignored only between classes that are specified before the base class with the IgnoreDuplicates keyword. The IgnoreDuplicates keyword in the following declaration accomplishes nothing, because there are no base classes before the Animal class:

class pet extends animal inherit ignoreDuplicates and property inherit

A naming collision between the Animal class and the Property class would still cause a compilation error.

When IgnoreDuplicates is specified for an Inherit base class, any member names in that class that match an earlier Inherit base class member name will not be accessible without qualification, if they are being accessed via extension class variables. For example, a class has the following declaration:

class pet extends animal inherit and property inherit ignoreDuplicates

If both the Animal and Property classes have a member called Weight, the following call references the Weight member of the Animal class (assuming the Pet class has no Weight member that hides Animal's Weight member):

%tinky is object pet ... print %tinky:weight

In this case, the Property class's Weight member is accessible by qualifying it with the class name:

%tinky is object pet ... print %tinky:(property)weight

Using IgnoreDuplicates can be risky if the base classes in question are not maintained to avoid name collisions, because the addition of a member to the earlier base class can break existing code. For example, if the Property class above has a function with no parameters called Value, and the Animal class has no eponymous member, the following statements readily compile:

%tinky is object pet ... print %tinky:value

However, if someone adds a subroutine called Value to the Animal class, the Print statement above would fail to compile. Since the IgnoreDuplicates clause in the Class statement dictates that an Animal class Value pre-empts a Property class one, the compilation failure would be because the value of a subroutine cannot be printed.

Using multiple Inherits and IgnoreDuplicates without care can be even more hazardous: if a function, property, or variable called Value were added to the Animal class in the preceding example, the code above would compile but start to work differently, possibly producing subtle and difficult to diagnose problems.

Repeat inheritance

With multiple inheritance, it is possible to define an extension class where the same base class appears more than once in the inheritance tree. For example, if the class MaleDriver extends the Driver class and the class AmericanDriver extends the Driver class, it might seem logical to create a MaleAmericanDriver class that extends both the MaleDriver and the AmericanDriver class. If this were done, however, the MaleAmericanDriver would end up with the Driver class as a base class twice — once via the MaleDriver class, and once via the AmericanDriver class. This is an example of repeat inheritance and was not allowed by SOUL until support was introduced in Sirius Mods version 7.8.

While the past lack of support for repeat inheritance might have seemed a significant problem, many cases where repeat inheritance seems necessary are actually cases where the class model is simply wrong. In the above example, it would probably have made more sense for there to be a Male class and an American class. In that case, the MaleDriver class would have extended the Male and Driver classes, the AmericanDriver class could have extended the American and Driver classes, and the MaleAmericanDriver class could have extended the Male, American, and Driver classes, eliminating the need for repeat inheritance.

The repeat-inheritance-free model, above, has many other advantages. First, it makes it much easier to define other classes, such as a MaleAmerican class, or a MexicanDriver class. Second, it reduces inheritance tree depth by one level, which makes coding and debugging easier. Finally, it just makes more sense — maleness is orthogonal to whether or not someone drives, so why should there be a MaleDriver class but no Male class?

Still, there might be some special cases where repeat inheritance does make sense so SOUL supports repeat inheritance, with some restrictions. The chief restriction on repeat inheritance is that if a class is repeatedly inherited by another class, the inherited class must only be inherited multiple times under a class that extends the class directly.

The most common case is likely to be a repeatedly inherited class extended directly by the outermost extension class. For example, if in the above example, AmericanMale extended the Male class and MaleDriver also extended the Male class, and the AmericanMaleDriver class extended MaleDriver and AmericanMale, then it would also have to directly extend the Male class. In other words, the Class statement for the AmericanMaleDriver class would have to look something like:

class americanMaleDriver extends americanMale inherit - and maleDriver inherit - and male inherit

If a class AmericanMaleTruckDriver extended AmericanMaleDriver (and no other classes) that would be allowed — while Male is repeatedly inherited, all repeat inheritances are under the AmericanMaleDriver class which extends the Male class directly.

It would also be valid if AmericanMaleTruckDriver extended AmericanMaleDriver and Worker, assuming Worker did not also extend the Male class. If, in fact, AmericanMaleTruckDriver extended AmericanMaleDriver and MaleWorker, and MaleWorker extended the Male class (as would be expected), then AmericanMaleTruckDriver would also have to extend the Male class directly:

class americanMaleTruckDriver extends americanMaleDriver inherit - and maleWorker inherit - and male inherit

Of course, in real world applications this level of inheritance would be exceedingly rare and would suggest an overenthusiasm for inheritance that might ultimately lead to trouble,

Regardless, if repeat inheritance does rear its head, the programmer should be aware of the rules for variable and method references in the repeatedly inherited class.

First, any instance (non-shared) variables in a repeatedly inherited class will have multiple copies in the extending class — one for each repeated inheritance, regardless of the depth. For example, suppose BirthDate is a variable in the Male class (this is silly, of course, because females would also have BirthDates). Then the AmericanMaleTruckDriver class would have four BirthDate variables: one under the AmericanMale class, one under the MaleDriver class, one under the AmericanMaleDriver class, and one under the AmericanMaleTruckDriver class. The one that is actually referenced depends on the class of the object variable used to reference the variable. For example, given the following:

%maleDriver is object maleDriver %americanDriver is object americanDriver %americanMaleDriver is object americanMaleDriver %americanMaleTruckDriver is object americanMaleTruckDriver

Then %maleDriver:birthDate would reference the BirthDate directly under the MaleDriver class, while %americanMaleDriver:birthDate would reference the BirthDate directly under the AmericanMaleDriver class, and so on. So, if we did:

%americanMaleDriver = %americanMaleTruckDriver %americanMale = %americanMaleDriver %americanMaleTruckDriver:birthDate = '19721106' %americanMaleDriver:birthDate = '19930529' %americanMale:birthDate = '19580212' printText {~} = {%americanMaleTruckDriver:birthDate} printText {~} = {%americanMaleDriver:birthDate} printText {~} = {%americanMale:birthDate}

The following would be printed:

%americanMaleTruckDriver:birthDate = 19721106 %americanMaleDriver:birthDate = 19930529 %americanMale:birthDate = 19580212

Of course, this doesn't make a heck of a lot of sense and the oddity of it all should make one uneasy. It suggests that one should be very cautious about the use of variables in classes that are repeatedly inherited. In any case, the variable referenced in a repeatedly inherited class is the copy in the inherited class that is most directly extended by the class of the object variable used in the reference.

Similarly, if a non-overridable method is invoked, the question arises as to which repeated Male class's variables are referenced by the %this passed to the method. The answer is again, of course, the copy of the inherited class that is most directly extended by the class of the object variable used in the reference. So, in the above example, if the Male class had this method:

subroutine showBirthDate printText The birth date is: {%this:birthDate} end subroutine

Then if you specify the following:

%americanMaleTruckDriver:showBirthDate %americanMaleDriver:showBirthDate %americanMale:showBirthDate

The result is:

The birth date is: 19721106 The birth date is: 19930529 The birth date is: 19580212

For overridable (or abstract) methods, regardless of the class of the object variable used to reference it, %this refers to the copy of the inherited class that is most directly extended by the class of the object referenced. For example, if the Male class had this method:

function birthYear overridable return %this:birthDate:left(4) end function

And these statements are executed:

printText {~} = {%americanMaleTruckDriver:birthYear} printText {~} = {%americanMaleDriver:birthYear} printText {~} = {%americanMale:birthYear}

The result is the following, because all the object variables reference an AmericanMaleTruckDriver object:

%americanMaleTruckDriver:birthYear = 1972 %americanMaleDriver:birthYear = 1972 %americanMale:birthYear = 1972

If the AmericanMale class had a method that implemented (overrode) the BirthYear method, it would still not be invoked in this case, because it overrides the method in the copy of the Male class that's extended by the AmericanMale class. However, if the AmericanMaleTruckDriver class had this method:

function birthYear implements birthYear in male return 1992 end function

And these statements are executed:

printText {~} = {%americanMaleTruckDriver:birthYear} printText {~} = {%americanMaleDriver:birthYear} printText {~} = {%americanMale:birthYear}

The following would be printed, because the BirthYear method in the AmericanMaleTruckDriver class overrides the BirthYear method in the outermost copy of the Male class:

%americanMaleTruckDriver:birthYear = 1992 %americanMaleDriver:birthYear = 1992 %americanMale:birthYear = 1992

Interfaces

Multiple inheritance provides all the capabilities that interfaces provide with more flexibility. Specifically, an interface is simply a class in which all methods are abstract (and so must be overridden) and which contains no variables. In SOUL, it is easy enough to define an abstract class that has no variables and in which all methods are abstract. This pretty much provides the exact same functionality as interfaces, meaning that there is really no compelling reason to have support for special interface classes in SOUL.

The main reason for the existence of interfaces in other languages is the lack of support for multiple inheritance. In such languages, while there is no generalized multiple inheritance support, there is support for multiple inheritance of interface classes, classes which have no data and in which all methods are abstract. These restrictions avoid most of the semantic and implementation issues that arise with support for true multiple inheritance. And, while one might argue that these restrictions are a good thing, there is nothing in SOUL that prevents one from adhering to these restrictions by choice.

On the other hand, it is also conceivable that some of the restrictions on interfaces could cause problems. For example:

  • The fact that all methods in an interface must be abstract means that if one wants to add a method to an interface, all extenders of that interface must be updated to provide an implementation of that new method. Obviously, this is likely to make interfaces somewhat brittle.

    With SOUL, you can use an abstract class with no data exactly as an interface. But if you want to add a method, you can add a default implementation for that method and make it overridable, not abstract. This means that methods can easily be added to interface-like classes in SOUL without creating a rippling effect that requires updating of all extenders of the class.

  • The absence of non-abstract methods in interfaces seems like a less important restriction than the absence of instance variables in avoiding the semantic and implementation issues brought about by multiple inheritance. As such, it is quite possible that at least this restriction would be dropped some day in languages that support interfaces but not multiple inheritance.

  • The absence of instance-specific variable support for interfaces can also be a problem.

    For example, considered a ScheduledObject class where one can use a generic queue manager to schedule activities on the object. Since the actual activities would be class-specific, it would seem like an inheritance model would make a lot of sense — the class would provide overrides of standard methods in the ScheduledObject class to perform a specific activity. But, it would also seem that the ScheduledObject class would really need instance-specific variables such as queue pointers, status flags, priorities, time stamps and the like.

    Of course, this is generally not a problem. Any class that wants to be a ScheduledObject would simply extend the ScheduledObject class. However, it's not inconceivable that one might have a class that might sometimes be used as a ScheduledObject, and sometimes not.

    For example, one might have a Payment class that is sometimes used for display purposes. That is, it might be loaded from a database file and then sent to a web page. On the other hand, sometimes Payment actions need to be scheduled and, for that purpose, it would be useful to have a class that was both a Payment and a ScheduledObject, say a ScheduledPayment. With multiple inheritance, this is easily accomplished, but with interfaces this is impossible because both the Payment class and the ScheduledObject class have variables so neither can be an interface, so no class can extend both of them. The solution would be to simply make the Payment class extend the ScheduledObject class. While this means that Payment objects always carry around the baggage of the ScheduledRequest class, it's probably an acceptable solution in most cases.

    It becomes problematic, however, if there's yet another class that Payment needs to extend like, say, a BusinessTransaction class. That class would likely have its own variables and so, again, would not lend itself to being implemented as an interface.

All this said, a large amount of inheritance is not common in business applications, and in the vast majority of classes, neither interfaces nor multiple inheritance would be necessary. However, in the odd cases where they are, multiple inheritance provides everything interfaces do and then some. For this reason, and because SOUL supports multiple inheritance, there is no need for SOUL to also provide support for interfaces.

Inheritance and constructors

An extension class might or might not have any constructors. If an extension class has no constructors, the default (New) constructors for the base classes are called in sequence with no parameters. However, if any of the base class constructors require parameters or don't have the default New constructor, a constructor is required for the extension class. Of course, even if a constructor is not required for an extension class, one can be provided as needed.

In any case, if an extension class has a constructor, that constructor is required to complete the construction of the object for all base classes. This is accomplished with the Construct statement, which is followed by an invocation of the shared New method or by invocation of any other constructor. The Construct statement must be followed by the class name (using the shared method invocation syntax), followed by the constructor name, followed by any constructor parameters.

For example, if extension class FooBar, of classes Foo and Bar, had a constructor, that constructor might look like:

constructor new construct %(bar):new construct %(foo):new end constructor

Construct statements are required for both the Foo and Bar class, whether or not they have explicit constructors. If the Bar class New constructor had a parameter, the constructor might look like:

constructor new(%height is float) construct %(bar):new(%height) construct %(foo):new end constructor

It is also possible to invoke constructors other than New. For example, if the Foo class had a NewFoo constructor, the following would be allowed:

constructor new(%height is float) construct %(bar):new(%height) construct %(foo):newFoo end constructor

It is possible to have statements in addition to Construct statements in an an extension class constructor:

constructor new(%height is float) %height = %height + 1 construct %(bar):new(%height) print 'Returned from Bar constructor' construct %(foo):newFoo print 'Returned from Foo constructor' end constructor

And, in fact, the Construct statements can be inside of If clauses:

constructor new(%height is float) if %height lt 10 then construct %(bar):new(10) else construct %(bar):new(%height) end if construct %(foo):newFoo end constructor

There are three basic inheritance-specific rules for constructors:

  • It is invalid to refer to a base class member until the constructor for that base class has been entered, that is, until a Construct statement has been issued for that base class.
  • It is invalid to invoke multiple Construct statements against the same base class for the same object instance.
  • When exiting from an extension class constructor, Construct statements must have been issued for all base classes for the object.

Formerly enforced at compile-time, violations of these rules are now caught at run-time. This allows more flexibility in writing constructors at the cost of the compile-time checks for rules conformance.

The one perhaps somewhat arbitrary rule that still remains is that a Construct statement must be physically placed inside a constructor.

The following subsections describe the restrictions concerning extension class constructors. Because the inheritance-specific constructor rules are enforced at run-time in Sirius Mods 7.1 and later, these rules do not apply to those releases. For earlier releases, while the rules for extension class constructors might seem complex, they probably will not come into play outside of fairly complicated constructors. Furthermore, any violation of the rules were detected at compile-time with a clear error message indicating the problem. Consequently, it is recommended that not too much time be spent learning these rules until they actually come into play.

Any program that followed the compile-time enforced rules set out in the following sections under Sirius Mods release before 7.1 are guaranteed to meet the run-time tests introduced under Sirius Mods 7.1. This ensures that there are no backward compatibility issues — any constructor that worked before Sirius Mods 7.1 would continue to work under Sirius Mods 7.1 and later.

Restrictions on the placement of Construct statements

In Sirius Mods releases before 7.1 there were certain restrictions on statements in an extension class constructor. These restrictions were intended to ensure that exactly one Construct statement was executed for each base class in an extension class constructor: These restrictions were:

  • Only one Construct statement was allowed per base class, unless the statements are in mutually exclusive If, ElseIf, or Else clauses.
  • No labels were allowed before a Construct statement.
  • No Jump statements were allowed before a Construct statement.
  • Construct statements could not be inside of For or Repeat loops.
  • If a Construct statement for a base class appeared in an If, ElseIf, or Else clause, Construct statements for that base class must also have been present in every other If, ElseIf, and Else clause for that block, and there must be an Else clause for that block.

These restrictions were validated at compile-time, so if a constructor compiled without errors, it must have satisfied these conditions. For example, the following definition would result in a compilation error, because there was no Else clause for the If statement, so the Construct statement for the Bar class might never be run:

constructor new(%height is float) if %height lt 10 then construct %(bar):new(10) end if construct %(foo):newFoo end constructor

The following definition would also result in a compilation error, because the ElseIf clause for the If statement had no Construct statement for the Bar class:

constructor new(%height is float) if %height lt 10 then construct %(bar):new(10) elseIf %height gt 200 then print 'Why bother?' else construct %(bar):new(%height) end if construct %(foo):newFoo end constructor

The following definition would compile without errors:

constructor new(%height is float) if %height lt 10 then construct %(bar):new(10) elseIf %height gt 200 then construct %(bar):new(200) else construct %(bar):new(%height) end if construct %(foo):newFoo end constructor

Restrictions on references to base or extension class members

In addition to restrictions on the placement of Construct statements, in an extension class constructor, in Sirius Mods releases before 7.1 there were also some limitations on references to base or extension class members inside the extension class constructor. These restrictions were intended to prevent references to members of classes that are “incomplete” because the constructor had not been run for the object yet. The restrictions were:

  • No references could be made to a base class member before the Construct statement for the base class.
  • No references could be made to extension class members before all Construct statements for all base classes.

For example, suppose the Bar class had a member called Mitzvah. The following definition would result in a compilation error, because there was a reference to a Bar class member before the Construct statement for the Bar class;

constructor new(%height is float) print 'Mitzvah = ' with %this:(bar)mitzvah construct %(bar):new(%height) construct %(foo):newFoo end constructor

But the following definition was valid, because the Mitzvah reference came after the Construct for the Bar class:

constructor new(%height is float) construct %(bar):new(%height) print 'Mitzvah = ' with %this:(bar)mitzvah construct %(foo):newFoo end constructor

If the FooBar class had a member called SchmooBar, the following definition was not valid, because the reference to that member occurred before the Construct for the Bar class:

constructor new(%height is float) construct %(foo):newFoo if %height lt 10 then print 'SchmooBar = ' with %this:SchmooBar construct %(bar):new(10) else construct %(bar):new(%height) end if end constructor

However, the following was perfectly valid, because the Construct for both the Foo and Bar classes would occur before the SchmooBar reference:

constructor new(%height is float) construct %(foo):newFoo if %height lt 10 then construct %(bar):new(10) print 'SchmooBar = ' with %this:SchmooBar else construct %(bar):new(%height) end if end constructor

The Construct statement in the Else clause, above, was not a problem, since it would never be executed for the same object for which the SchmooBar reference is made.

All the member reference restrictions were detected at compile-time, so if an extension class constructor compiled successfully, you could be sure that all the rules have been observed.

Restrictions on %this variable assignments

In Sirius Mods releases before 7.1, the final restrictions on extension class constructors were that the %this variable could not be assigned to a variable that references

  • a base class member, unless the Construct statement for the base class had been issued. If allowed, this would be a polymorphic assignment (as described in the next section).
  • an extension class member, unless all Construct statements for all base classes had been issued.

So, the following assignment to %myFoo was invalid, because it occurred before the Construct for the Foo class:

constructor new(%height is float) %myFoo is object foo construct %(bar):new(%height) %myFoo = %this construct %(foo):newFoo end constructor

However, the following assignment to %myBar was valid, because it occurred after the Construct for the Bar class:

constructor new(%height is float) %myBar is object bar construct %(bar):new(%height) %myBar = %this construct %(foo):newFoo end constructor

The following assignment to %myFooBar was invalid, because it occurred before the Construct for the Foo class (all base classes had to be constructed before assignment from %this to a variable of the extension class):

constructor new(%height is float) %myFooBar is object fooBar construct %(bar):new(%height) %myFooBar = %this construct %(foo):newFoo end constructor

However, the following assignment to %myFooBar was valid, because it occurred after the Construct for all of FooBar's base classes:

constructor new(%height is float) %myFooBar is object fooBar construct %(bar):new(%height) construct %(foo):newFoo %myFooBar = %this end constructor

All %this variable assignment restrictions were detected at compile-time, so if an extension class constructor compiled successfully, you could be sure that all the rules had been observed.

Note:

A different but related restriction on %this assignments concerns base class constructors called by Construct statements. As of Sirius Mods version 7.3, the request is canceled if %this is changed by assignment within a base class constructor. Because an extension class constructor that does a Construct has its own implicit or explicit %this to reference the extension object being constructed, assigning to %this in the base class constructor cannot be allowed to have an effect on the extension class.

For example, a base class constructor might use %this = null to signal a problem in the base object construction. Prior to version 7.3, if this constructor were called via Construct and %this were set to null, this construction problem would be ignored and the program would proceed unknowingly.

An alternative to using %this=null in this case is to use %this:discard. Another alternative is to throw an exception. A Throw statement that executes within a constructor causes an automatic Discard of the object being constructed.

Polymorphism

Polymorphism is the ability of an object variable to refer to objects of the variable's class, or to objects of an extension class of the variable's class. For example, if there is a Mammal class, and the Otter class extends the Mammal class, an object variable of the Mammal class could refer to a Mammal object or an Otter object (since all Otters are Mammals). An Otter object variable can always be assigned to a Mammal variable:

%tarka is object otter %fuzzy is object mammal ... %tarka = new ... %fuzzy = %tarka

After the assignment, the %fuzzy variable can be used as any other Mammal object variable, even though the referenced object is actually an Otter — again, all Otters are Mammals. Note, however, that the %fuzzy variable cannot be used to invoke Otter-specific methods, because the determination of which method is to be run for a method invocation is done at compile-time, and the compiler has no way of knowing that %fuzzy would end up referencing an Otter object.

Polymorphism can also be used in cases of implied assignments. For example, if there is a method in class Zoo that takes a Mammal input object:

class zoo subroutine brushFur(%animal is object mammal) end class

It is possible to invoke this method passing an Otter variable as the input parameter:

%tarka is object otter %bronx is object zoo ... %tarka = new ... %bronx:brushFur(%tarka)

This is logically equivalent to assigning %tarka to the %animal input parameter.

A slight variant on this is when a base class method is called via an extension class variable, either via the Inherit keyword in the Extends clause, an Inherit statement in the extension class Public or Private block, or an explicit base class reference on the invocation.

For example, if the Otter class is defined as follows:

class otter extends mammal inherit

And if there is a LiveBirth method in the Mammal class, the following code calls that method:

%tarka is object otter ... %tarka = new ... %tarka:liveBirth

Part of this call is equivalent to polymorphic assignment of the %tarka variable of class Otter to the %this variable of class Mammal in the LiveBirth method. Exactly the same kind of implied assignment is done if the method invocation is via an explicit class specification:

%tarka is object otter ... %tarka = new ... %tarka:(mammal)liveBirth

All the examples discussed here are explicit or implicit assignments from extension class variables to base class variables. This kind of assignment is sometimes called a widening assignment: an extension class is considered to be a subset of the base class (again, think Otter and Mammal), and assignment from an extension class variable to a base class variable is from a more specific class to a more general class, hence a widening assignment.

Some object-oriented languages also allow what are called narrowing assignments — assignments from a base class variable to an extension class variable. Before &sirmods. version 6.9, SOUL did not allow such assignments. (For more information about the support that was added for such assignments in Sirius Mods Version 6.9, see "Narrowing assignments and class tests". That is, the following assignment from %fuzzy to %tarka is not allowed and results in a compilation error:

%tarka is object otter %fuzzy is object mammal ... %fuzzy = %(otter):new %tarka = %fuzzy

While initially this might seem like a rather arbitrary and problematic limitation on polymorphism, there are some good reasons why this is not the case:

  • It is exceedingly rare that one would even knowingly try such an assignment, much less need to do such an assignment.
  • Such an assignment might fail.

In the above example, it's clear from the code that %fuzzy would reference an Otter object. But, suppose %fuzzy actually contained a reference to a Kangaroo object, which is also a Mammal but is most definitely neither a base nor an extension class of Otter. It would seem clear that the assignment should fail and that, in the general case, the failure of the assignment could only be determined at run-time.

Languages that allow narrowing assignments simply compile the assignments, and if the assignment fails, produce a run-time error. Such languages make it the programmer's job to write code in such a way as to avoid such errors. Many languages that allow narrowing assignments, require the assignments to use a special syntax for the narrowing assignments to make it clear that the programmer meant to do what is being done, and to make it clear to anyone reading the code that something "funny" is going on.

Unfortunately, in the case of narrowing assignments, almost anything a programmer is likely to do to prevent narrowing assignment errors is likely to have bad code maintainability implications. To understand why, it is worth continuing the Otter and Mammal example.

Presumably, the programmer does not want request cancellation errors as a matter of course, so would probably do something like the following:

%tarka is object otter %skippy is object kangaroo %fuzzy is object mammal ... if %fuzzy is instance of otter then %tarka = %fuzzy ... do some otter specific processing end if

Note: Formerly, the "is instance of" test, above, was not provided by SOUL, nor was any other method that allowed one to determine programmatically the "true" class of an object. This intentional omission was to prevent just the sort of innocent looking, but ultimately, very troublesome code illustrated in the example.

To understand why the above example code is troublesome, consider the following scenario:

  1. The code works as needed and some special processing is done for Otter objects.
  2. There is other Otter-specific processing. That processing could be handled by If statements placed wherever needed.
  3. Someone wants to extend the Mammal class with, say, a Ferret class. The theory of object-oriented programming is that the Ferret class implementer simply has to understand the Mammal class to do what she needs to do. But due to all the If tests for the Otter class, adding a new extension class to Mammal is quite difficult. Which, if any, of the If clauses that are executed for Otters should be executed for Ferrets? Probably, some of them should be (after all, ferrets are very similar to otters; both are mustelids, that is, members of the weasel family). And probably some of them should not be executed (most otters are aquatic, ferrets are not).

To add a new extension class to the Mammal class, the coder of the Ferret class has to worry about all the If tests scattered throughout code that has nothing to do with the Mammal class. In fact, a successful implementation of the Ferret class might require modifying the code that has the narrowing assignments, adding extra ElseIf clauses with Ferret-specific processing.

If the approach in this scenario seems antithetical to the idea of encapsulating object behavior in the class definition, it is. If it seems like it would end up defeating many of the benefits of object-oriented programming, it would. This is why SOUL initially did not support narrowing assignments — they are a formula for defeating many of the benefits of object-oriented programming.

An astute reader will note that it is not so much the narrowing assignments that cause the long-term maintainability problems, as it is the If statements that make narrowing assignments possible to do safely. In fact, If clauses based on an object's "true" class can cause maintainability problems whether or not there are narrowing assignments inside the If clauses. This is why SOUL initially did not provide any methods for determining an object's "true" class: they would almost certainly be used to defeat the encapsulation of an object's behavior in the class definition.

If a class, such as the Mammal class, requires extension-class specific processing, a far superior approach to If tests based on the object's class is to use an approach called dynamic dispatch. This approach is sometimes called dynamic dispatch according to object type, and it is the topic of the Dynamic dispatch section.

All this said, narrowing assignments can sometimes be useful, possibly in infrastructure classes that are extended by many classes, though, arguably this is just the sort of case where one should go to the trouble of fully formalizing class behavior via dynamic dispatch, rather than using the questionable techniques of class tests and narrowing assignments. Perhaps better arguments for narrowing assignments would be "quick and dirty" applications where formalizing all class behavior would be overkill, or base classes that are not intended to be extended by any but a specific set of other classes. In any case, should the need for narrowing assignments or class tests present themselves in an application, SOUL provides support for such assignments. For more information, see Narrowing assignments and class tests.

See also