Does Polymorphism Make Code More Flexible?

Posted on by Chris Warburton

This is based on my answer to a Stack Overflow question about subtype-polymorphism in Object Oriented Programming.

Here's my summary of the question:

Head First Object Oriented Design explains polymorphism with this example:

Airplane plane = new Airplane();
Airplane plane = new Jet();
Airplane plane = new Rocket();

You can write code that works on the superclass, like an Airplane, but will work with any of the subclasses.

Now I am not getting it. I need to create a sublass of an Airplane. For example: I create a class, Randomflyer. To use it I will have to create its object. So I will use:

Airplane plane = new Randomflyer();

How does using a superclass save me from making extra changes to the rest of my code?

It's completely correct that sub-classes are only useful to those who instantiate them. This was summed up well by Rich Hickey:

...any new class is itself an island; unusable by any existing code written by anyone, anywhere. So consider throwing the baby out with the bath water.

It is still possible to use an object which has been instantiated somewhere else. As a trivial example of this, any method which accepts an argument of type Object will probably be given an instance of a sub-class.

There is another problem though, which is much more subtle. In general a sub-class (like Jet) will not work in place of a parent class (like Airplane). Assuming that sub-classes are interchangable with parent classes is the cause of a huge number of bugs.

This property of interchangability is known as the Liskov Substitution Principle, and was originally formulated as:

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

In the context of your example, T is the Airplane class, S is the Jet class, x are the Airplane instances and y are the Jet instances.

The "properties" q are the results of the instances' methods, the contents of their properties, the results of passing them to other operators or methods, etc. We can think of "provable" as meaning "observable"; ie. it doesn't matter if two objects are implemented differently, if there is no difference in their results. Likewise it doesn't matter if two objects will behave differently 'after' an infinite loop, since that code can never be reached.

Defining Jet as a sub-class of Airplane is a trivial matter of syntax: Jet's declaration must contain the extends Airplane tokens and there mustn't be a final token in the declaration of Airplane. It is trivial for the compiler to check that objects obey the rules of sub-classing. However, this doesn't tell us whether Jet is a sub-type of Airplane; ie. whether a Jet can be used in place of an Airplane, as Liskov requires. Java will allow it, but that doesn't mean it will work.

One way we can make Jet a sub-type of Airplane is to have Jet be an empty class; all of its behaviour comes from Airplane. However, even this trivial solution is problematic: an Airplane and a trivial Jet will behave differently when passed to the instanceof operator. Hence we need to inspect all of the code which uses Airplane to make sure that there are no instanceof calls. Of course, this goes completely against the ideas of encapsulation and modularity; there's no way we can inspect code which may not even exist yet!

Normally we want to sub-class in order to do something differently to the superclass. In this case, we have to make sure that none of these differences are observable to any code using Airplane. This is even more difficult than syntactically checking for instanceof; we need to know what all of that code does.

That's impossible due to Rice's theorem, hence there's no way to check sub-typing automatically, and hence the amount of bugs it causes.

For these reasons, many see sub-class polymorphism as an anti-pattern. There are other forms of polymorphism which don't suffer these problems though, for example Parameteric polymorphism (referred to as "generics" in Java).