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:
= new Airplane(); Airplane plane = new Jet(); Airplane plane = new Rocket(); Airplane plane
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:= new Randomflyer(); Airplane plane
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).