Inheritance and polymorphism: Difference between revisions
No edit summary |
|||
Line 1: | Line 1: | ||
Support for inheritance and polymorphism was added to <var class="product"> | Support for inheritance and polymorphism was added to <var class="product">SOUL</var> in | ||
<var class="product">Sirius Mods</var> version 6.7. | <var class="product">Sirius Mods</var> version 6.7. | ||
Line 200: | Line 200: | ||
</ul> | </ul> | ||
==Multiple inheritance== | ==Multiple inheritance== | ||
<var class="product"> | <var class="product">SOUL</var> allows a class to extend more than a single class. | ||
This seems reasonable, as many objects in the real world can be thought of | This seems reasonable, as many objects in the real world can be thought of | ||
as extensions to more than one base class. | as extensions to more than one base class. | ||
Line 208: | Line 208: | ||
class and to a Property class. | class and to a Property class. | ||
To extend multiple classes with <var class="product"> | To extend multiple classes with <var class="product">SOUL</var>, you separate the extended classes | ||
with the <var>And</var> keyword in the <var>Class</var> statement <var>Extends</var> clause: | with the <var>And</var> keyword in the <var>Class</var> statement <var>Extends</var> clause: | ||
<p class="code">class pet extends animal and property | <p class="code">class pet extends animal and property | ||
Line 295: | Line 295: | ||
but start to work differently, possibly producing subtle and difficult | but start to work differently, possibly producing subtle and difficult | ||
to diagnose problems. | to diagnose problems. | ||
==Repeat inheritance== | ==Repeat inheritance== | ||
With multiple inheritance, it is possible to define an extension class | With multiple inheritance, it is possible to define an extension class | ||
Line 307: | Line 307: | ||
class, and once via the AmericanDriver class. | class, and once via the AmericanDriver class. | ||
This is an example of '''repeat inheritance''' and was <i><b>not</b></i> | This is an example of '''repeat inheritance''' and was <i><b>not</b></i> | ||
allowed by the <var class="product"> | allowed by the <var class="product">SOUL</var> before <var class="product">Sirius Mods</var> version 7.8. | ||
<var class="product">Sirius Mods</var> version 7.8, however, <i><b>does</b></i> support repeat inheritance. | <var class="product">Sirius Mods</var> version 7.8, however, <i><b>does</b></i> support repeat inheritance. | ||
Line 331: | Line 331: | ||
Still, there might be some special cases where repeat inheritance does | Still, there might be some special cases where repeat inheritance does | ||
make sense so, as of <var class="product">Sirius Mods</var> version 7.8, <var class="product"> | make sense so, as of <var class="product">Sirius Mods</var> version 7.8, <var class="product">SOUL</var> supports repeat inheritance, | ||
with some restrictions. | with some restrictions. | ||
The chief restriction on repeat inheritance is that if a class is | The chief restriction on repeat inheritance is that if a class is | ||
Line 488: | Line 488: | ||
are abstract (and so must be overridden) and which contains no | are abstract (and so must be overridden) and which contains no | ||
variables. | variables. | ||
In <var class="product"> | In <var class="product">SOUL</var>, it is easy enough to define an abstract class that has | ||
no variables and in which all methods are abstract. | no variables and in which all methods are abstract. | ||
This pretty much provides the exact same functionality as interfaces, | This pretty much provides the exact same functionality as interfaces, | ||
meaning that there is really no compelling reason to have support for | meaning that there is really no compelling reason to have support for | ||
special interface classes in <var class="product"> | special interface classes in <var class="product">SOUL</var>. | ||
The main reason for the existence of interfaces in other languages | The main reason for the existence of interfaces in other languages | ||
Line 502: | Line 502: | ||
that arise with support for true multiple inheritance. | that arise with support for true multiple inheritance. | ||
And, while one might argue that these restrictions are a good thing, | And, while one might argue that these restrictions are a good thing, | ||
there is nothing in <var class="product"> | there is nothing in <var class="product">SOUL</var> that prevents one from adhering to | ||
these restrictions by choice. | these restrictions by choice. | ||
Line 515: | Line 515: | ||
Obviously, this is likely to make interfaces somewhat brittle. | Obviously, this is likely to make interfaces somewhat brittle. | ||
<p> | <p> | ||
With <var class="product"> | With <var class="product">SOUL</var>, 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 | 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. | implementation for that method and make it overridable, not abstract. | ||
This means that methods can easily be added to interface-like classes | This means that methods can easily be added to interface-like classes | ||
in <var class="product"> | in <var class="product">SOUL</var> without creating a rippling effect that requires updating of | ||
all extenders of the class. | all extenders of the class. | ||
<li>The absence of non-abstract methods in interfaces seems like a less | <li>The absence of non-abstract methods in interfaces seems like a less | ||
important restriction than the absence of instance variables in | important restriction than the absence of instance variables in | ||
Line 542: | Line 542: | ||
need instance-specific variables such as queue pointers, status flags, | need instance-specific variables such as queue pointers, status flags, | ||
priorities, time stamps and the like.</p> | priorities, time stamps and the like.</p> | ||
<p> | <p> | ||
Of course, this is generally not a problem. | Of course, this is generally not a problem. | ||
Any class that wants to be a ScheduledObject would simply extend the | Any class that wants to be a ScheduledObject would simply extend the | ||
Line 566: | Line 566: | ||
the baggage of the ScheduledRequest class, it's probably an acceptable | the baggage of the ScheduledRequest class, it's probably an acceptable | ||
solution in most cases.</p> | solution in most cases.</p> | ||
<p> | <p> | ||
It becomes problematic, however, if there's yet another class | It becomes problematic, however, if there's yet another class | ||
that Payment needs to extend like, say, a BusinessTransaction class. | that Payment needs to extend like, say, a BusinessTransaction class. | ||
That class would likely have its own variables and so, again, would | That class would likely have its own variables and so, again, would | ||
not lend itself to being implemented as an interface.</p> | not lend itself to being implemented as an interface.</p> | ||
</ul> | </ul> | ||
All this said, a large amount of inheritance is not common in business | All this said, a large amount of inheritance is not common in business | ||
applications, and in the vast majority of classes, neither interfaces | applications, and in the vast majority of classes, neither interfaces | ||
Line 577: | Line 577: | ||
However, in the odd cases where they are, multiple inheritance provides | However, in the odd cases where they are, multiple inheritance provides | ||
everything interfaces do and then some. | everything interfaces do and then some. | ||
For this reason, and because <var class="product"> | For this reason, and because <var class="product">SOUL</var> supports multiple inheritance, | ||
there is no need for <var class="product"> | there is no need for <var class="product">SOUL</var> to also provide support for interfaces. | ||
==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>[[Janus SOAP User Language Interface#constructor|constructors]]</var>. | ||
Line 1,001: | Line 1,001: | ||
variable. | variable. | ||
Before &sirmods. version 6.9, | Before &sirmods. version 6.9, | ||
<var class="product"> | <var class="product">SOUL</var> did ''not'' allow such assignments. | ||
(For more information about the support that was added for such assignments in <var class="product">Sirius Mods</var> | (For more information about the support that was added for such assignments in <var class="product">Sirius Mods</var> | ||
Version 6.9, see [[#Narrowing assignments and class tests|"Narrowing assignments and class tests"]]. | Version 6.9, see [[#Narrowing assignments and class tests|"Narrowing assignments and class tests"]]. | ||
Line 1,062: | Line 1,062: | ||
<br>'''Note:''' | <br>'''Note:''' | ||
Before <var class="product">Sirius Mods</var> Version 6.9, the "is instance of" | Before <var class="product">Sirius Mods</var> Version 6.9, the "is instance of" | ||
test, above, was ''not'' provided by <var class="product"> | test, above, was ''not'' provided by <var class="product">SOUL</var>, nor was any | ||
other method that allowed one to determine programmatically the "true" | other method that allowed one to determine programmatically the "true" | ||
class of an object. | class of an object. | ||
Line 1,104: | Line 1,104: | ||
If it seems like it would end up defeating many of the benefits | If it seems like it would end up defeating many of the benefits | ||
of object-oriented programming, it would. | of object-oriented programming, it would. | ||
This is why <var class="product"> | This is why <var class="product">SOUL</var> initially did not support narrowing assignments — | ||
they are a formula for defeating many of the benefits of object-oriented programming. | they are a formula for defeating many of the benefits of object-oriented programming. | ||
Line 1,113: | Line 1,113: | ||
based on an object's "true" class can cause maintainability problems | based on an object's "true" class can cause maintainability problems | ||
whether or not there are narrowing assignments inside the <var>If</var> clauses. | whether or not there are narrowing assignments inside the <var>If</var> clauses. | ||
This is why <var class="product"> | This is why <var class="product">SOUL</var> initially did not provide any methods for determining an | ||
object's "true" class: they would almost certainly be used to defeat | object's "true" class: they would almost certainly be used to defeat | ||
the encapsulation of an object's behavior in the class definition. | the encapsulation of an object's behavior in the class definition. | ||
Line 1,137: | Line 1,137: | ||
provide support for such assignments. | provide support for such assignments. | ||
For more information see [[Narrowing assignments and class tests|"Narrowing assignments and class tests"]]. | For more information see [[Narrowing assignments and class tests|"Narrowing assignments and class tests"]]. | ||
==See also== | ==See also== | ||
<ul> | <ul> | ||
Line 1,146: | Line 1,146: | ||
<li>[[Classes and Objects]] | <li>[[Classes and Objects]] | ||
</ul> | </ul> | ||
[[Category:Overviews]] | [[Category:Overviews]] | ||
[[Category:Janus SOAP ULI topics]] | [[Category:Janus SOAP ULI topics]] |
Revision as of 02:54, 11 October 2013
Support for inheritance and polymorphism was added to SOUL in Sirius Mods version 6.7.
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 Language classes. There are exceptions, however. It is not possible to extend the following types of system classes:
- Collections
- Enumerations
- The XmlNode class
Before Sirius Mods 7.0, it was also not possible to extend
file classes, such as the Recordset class, the Record class, and the
RecordsetCursor class.
Under Sirius Mods 7.0 and later, 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 the SOUL before Sirius Mods version 7.8. Sirius Mods version 7.8, however, does support repeat inheritance.
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, as of Sirius Mods version 7.8, 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.
Before Sirius Mods 7.1, these rules were enforced at compile-time. The benefit of this was that a program that compiled successfully was known to follow the rules. Unfortunately, to make it practical for the compiler to ensure that the inheritance-specific constructor rules were being followed, a variety of other restrictions had to be added to constructors. In practice, some of these restrictions proved to be impediments to solving certain problems. As a result, Sirius Mods 7.1 removed almost all of these restrictions and changed the checks for rules violations to run-time checks. This allowed more flexibility in writing constructors but, unfortunately, eliminated 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:
Before Sirius Mods Version 6.9, 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:
- The code works as needed and some special processing is done for Otters.
- There is other Otter-specific processing. That processing could be handled by If statements placed wherever needed.
- 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, Sirius Mods Version 6.9 and later provide support for such assignments. For more information see "Narrowing assignments and class tests".