What is Polymorphism?
Polymorphism is the ability to represent different sets of data and different behaviors with a common interface. Typically, this means two classes that have the same methods but different implementations of those methods.
A good analogy is the accelerator pedal of a car. If you push on the pedal the car will go forward. Perhaps the car is an electric car; perhaps it’s a gas-powered car. But if you push on the pedal, the car goes forward. The driver doesn’t need to know how the car goes forward, just that it goes forward when you push on the pedal.
Strongly typed languages such as Java and C# allow formal definition of an interface, which other classes can then implement. Then, variables can be declared as the type of the interface, and instantiated as any class that implements it.
Polymorphism in Ruby
If you are used to a strongly typed language, you may find the approach to polymorphism of a weakly typed language such as Ruby a bit disorienting. There are no formal definitions of interfaces or abstract classes, and there is no specifying of type when declaring variables.
To accomplish polymorphic behavior, Ruby simply has subclasses override methods, or dispenses with inheritance altogether and uses duck typing.
Overriding Superclass Methods
In this example, various subclasses of Animal
implement a #move
method in different ways by overriding Animal
‘s #move
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class Animal def move puts 'stepping out' end end class Dog < Animal # uses the default implementation of `#move` provided by the superclass end class Flea < Animal def move puts 'boing boing' end end class TRex < Animal def move puts 'KABOOM....KABOOM' end end fido = Dog.new buzz = Flea.new igor = TRex.new [fido, buzz, igor].each { |e| e.move } # => stepping out # => boing boing # => KABOOM....KABOOM |
We get the polymorphic behavior, but we do not get compile or runtime checking beyond an error if a method that doesn’t exist gets called, as we would in a strongly typed language with the ability to formally define interfaces and implementations of interfaces.
Duck Typing
If we don’t have any common method implementations in a superclass, we can simply use duck typing. Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class AutoRace def initialize @cars = [] end def add_car(car) @cars << car end def start_engines @cars.each { |car| car.rev } end end class Ferrari def rev puts 'VVRROOOOAAAARRRRRRrrrrrr...VVRROOOOAAAARRRRRRrrrrrr...' end end class Porsche def rev puts 'WHIIIIIINNNEEee...WHIINNneeWHIINNneeWHIINNnee...' end end class VW def rev puts 'Whizz...Whizz...sputterWhizz...' end end race = AutoRace.new race.add_car(Ferrari.new) race.add_car(Porsche.new) race.add_car(VW.new) race.start_engines # => VVRROOOOAAAARRRRRRrrrrrr...VVRROOOOAAAARRRRRRrrrrrr... # => WHIIIIIINNNEEee...WHIINNneeWHIINNneeWHIINNnee... # => Whizz...Whizz...sputterWhizz... |
The cars’ #rev
method is an example of duck typing. We add the cars to an instance of an AutoRace
class as collaborator objects. (This isn’t strictly necessary. We could just create an array of cars in main
, and pass it into the #each
method, and call rev
from the block, and we would still have polymorphic behavior.) The AutoRace
object’s #start_engines
instance method calls the rev
method of each of the cars in turn, getting different behavior from each of them. Polymorphic behavior, in other words.
The point in these examples is that consumers (invokers) of a polymorphic method don’t have to know exactly which object’s method they are calling. The object just has to expose that method for the consumer to call. In the above example, the start_engines
method doesn’t know which object’s move
method it’s calling. It just knows there is a move
method.
That’s what makes polymorphism useful. It allows a consumer of a group of objects with the same method signature to be agnostic about what those objects do.