Does Polymorphism Make Code More Flexible?
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:
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: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 objectsx
of typeT
. Thenq(y)
should be provable for objectsy
of typeS
whereS
is a subtype ofT
.
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).