A look at Polymorphism as provided by Object Oriented languages and how one code structure (method, operator or reference) can have many implementations
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?
Polymorphism or “Many Forms” in OO means one of many actions can happen, and that the one chosen depends on the parameters, operands, or concrete type, rather than the method, the operator or the virtual type.
When refering to an object in abstract terms we often want they object to take a 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
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.
If we were to construct a
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.
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
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.
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.
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.
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
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.
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.
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.