Fundamental Object Oriented Design principles (Part 3): Polymorphism

6 minute read

A look at Polymorphism as provided by Object Oriented languages.

This is the third post in a series on Object Oriented design. In order to fully understand why the study of Design Patterns is important, we need to start by looking at what good Object Oriented (OO) software design is. Object Oriented languages offer some unique features that facilitate good design.

These design principles can be applied to non-objected oriented languages. However, applying OO concepts to non-OO languages takes some more effort.

What is Polymorphism?

When refering to an object in abstract terms we often want they object to take very specific action according to its concrete type rather than the type of reference to the object. Even when an object is accessed through an abstract type, the actual method of the specific instance type can be invoked. This is in essence polymorphism. As we saw in the section on abstraction we are allowed to interact with objects via variables that are ancestor classes to the actual class of the object, the problem is that type of action we want executed when a method is called needs to be the unique definition as provided by the object’s specific class (which we may not know).

The most common understanding of polymorphism is that methods can be virtual. This means that when a decendant class overrides the definition of a method it also replaces the ancestor’s implementation of the method with its own.

Virtual method overriding

We saw some of this when we looked at the concept of abstraction via an abstract method, but in that case the base method was declared as abstract and we had no code implementation behind it

type

TCurve = class
public
   function GetStartPoint: TPoint; virtual; abstract;   // TPoint is not defined in this post. 
end;

TArc = class(TCurve)
public
   function GetStartPoint: TPoint; override;   
end;

TLine = class(TCurve)
public
   function GetStartPoint: TPoint; override;   
end;

Let us use a similar structure but make the replacement of the method more clear where our method replaces the actual definition of an ancestor’s definition.

type

TDog = class
public
   function Bark: string; virtual;   // Woof!
end;

TPoodle = class(TDog)
public
  //this Bark method will replace the one in TDog	
  function Bark: string; override;   // Yap!
end;

TToyPoodle = class(TPoodle)
public
  //this Bark method will replace the one in TDog	
  function Bark: string; override; // Yip!  
end;

If we were to construct a TPoodle, TToyPoodle, an a generic TDog and reference them via variables of type TDog we can call Bark() we will get the actual method calls of the underlying object even if we don’t know what they are.

// if ADogList contains a TPoodle, a TDog and a TToyPoodle object will return "Yap! Woof! Yip!"
function MidnightChoir(ADogList: TList<TDog>) : string;
var
  LDog : TDog;
  LBarkString: string;
begin
  for LDog in ADogList do
	LBarkString := LBarkString + ' ' + LDog.Bark(); //we don't need to know the concrete type. The correct Bark will be called
  result := copy(LBarkString, 2, Length(LBarkString)-1);	 //remove the ' '
end;

Function overloading and Operator overloading

We don’t commonly think of overloading as a polymorphic helpers, but they allow us to add functionality to class while keeping the interface more concise. This type of polymorphis applies outside of object oriented design as well. The correct method call on the object is identified by the name of the function and a unique parameter signature defined for each overload of the method.

Here is a common example of function overloading polymorphism: You have stream object that reads into a number of variables. You could have a method ReadBoolean, ReadDouble, ReadInteger, etc., but that would require that you check the type of the variable and then find the appropriate function. Life can be made much easier if I could just call Read and have the correct call made depending on the parameter signature.

TMyStreamReader = class(TMyGenericStreamReader)
public
 function ReadData(var Buffer: Boolean): Longint; overload;
 function ReadData(var Buffer: Integer): Longint; overload;
 function ReadData(var Buffer: Single): Longint; overload;
 function ReadData(var Buffer: Double): Longint; overload;
end;

Operator overloading is identical to function overloading, but the syntax is a little different. Operators are usually not applied to classes. The left and right side of the infix operator become the two operands, the return result defines the operator output.

PointRecord = record
  x,y,z : double;
 
 class operator Multiply(P1: PointRecord; P2: PointRecord): Double; // scalar multiplication P1*P2
 class operator Multiply(P: PointRecord; d: double): PointRec; // scaling d*P
 class operator Multiply(d: double; P: PointRecord): PointRec; // scaling P*d
end;

Inheritance

We already saw that we can inherit a class from another and override the parent or ancestor class’ methods that are virtual. If we don’t override those methods we will inherit them from an ancestor class. Also if we declare a public non-virtual method on TDog (Pant()) we will have it visible when we reference our object via their specific classes as well.

type

TDog = class
public
  function Pant: string; 
  function Bark: string; virtual;   // Woof!
end;

THound = class(TDog) //inherits from TDog, but does not override Bark(). If we call Bark we will get Woof! as defined in TDog
end;

TPoodle = class(TDog)
public
 //this Bark method will replace the one in TDog	
 function Bark: string; override;   // Yap!
end;

TMaltesePoodle = class(TPoodle) //iherits from TPoodle, but does not override Bark(). If we call Bark we will get Yap! as defined in TPoodle
end;

Subtyping

Subtyping applies to the inheritence of interfaces. I think the name subtyping is unfortunate name because classes have an “is-a” relationship to their ancestors and each class is in essence a more specialized type of the parent. I think “interface extension” would be a technically more correct term. In this case the interface that inherits from another is really an extension of the contract, the inherited interface may add more requirements to the definition, but can take none away. In the contrived example below any object that implements IEquatable needs to also satisfy all of IComparable

type
  IComparable = interface(IInterface)
    function CompareTo(AObject: TObject): Integer;
  end;
  
  IEquatable = interface(IComparable)
    function EqualTo(AObject: TObject): Boolean;
  end;

Interface extension does not dictate the way that any class needs to implement the signatures of the functions specified, it only requires that they be present. Any object that satisfies a specific interface must also satisfy the full interface hierarchy.

There are cases where “interface inheritence” truly does represent subtyping of a conceptual type. This is the case where a Interface is defined similar to a pure abstract class. In that case we can have a psuedo “is-a” relationship between interfaces.

type
  IMyABCList = Interface
    //details not important. Assume it specifies indexing, adding and removing items 
  end;
  
  IMyEnumerableABCList = Interface(IMyABCList)
	function GetEnumerator: IEnumerator;
  end;  

The IMyEnumerableABCList truly does seem like a sub-type of IMyABCList. I can now apply this interface hierarchy to classes independently of their inheritence structure. The definitions of methods signatures and member properties are inherited from the super-type to the sub-type, but method implementation behavior is not defined in sub-typing (or with Interface in general).

Interfaces by themselves are polymorphic assistants since they do not contain any concrete code when we reference an object via an interface the actual implementation is satisfied by the object and it’s class hierarchy and we only know that the contractual obligations are filled.

Summary

Polymorphism is simply the reduction of methods from the many to the few, where the specialization is derived from the concrete type, the signature of parameters or the object that satisfies the interface.

Leave a Comment