Polymorphism - the provision of a single interface to entities of different types
Polymorphism is one of the fundamental features of object oriented programming, but what exactly does it mean? At its core, in Ruby, it means being able to send the same message to different objects and get different results. Let’s look at a few different ways to achieve this.
Inheritance
One way we can achieve polymorphism is through inheritance. Let’s use the template method to create a simple file parser.
First let’s create a GenericParser
class that has a parse
method. Since
this is a template the only thing this method will do is raise an exception:
class GenericParser
def parse
raise NotImplementedError, 'You must implement the parse method'
end
end
Now we need to make a JsonParser
class that inherits from GenericParser
:
class JsonParser < GenericParser
def parse
puts 'An instance of the JsonParser class received the parse message'
end
end
Let’s create an XmlParser
to inherit from the GenericParser
as well:
class XmlParser < GenericParser
def parse
puts 'An instance of the XmlParser class received the parse message'
end
end
Now let’s run a script and take a look at how it behaves:
puts 'Using the XmlParser'
parser = XmlParser.new
parser.parse
puts 'Using the JsonParser'
parser = JsonParser.new
parser.parse
The resulting output looks like this:
Using the XmlParser
An instance of the XmlParser class received the parse message
Using the JsonParser
An instance of the JsonParser class received the parse message
Notice how the code behaves differently depending on which child class receives
the parse
method. Both the XML
and JSON parsers modify
GenericParser
‘s behavior, which raises an exception.
Duck Typing
In statically typed languages, runtime polymorphism is more difficult to achieve. Fortunately, with Ruby we can use duck typing.
We’ll use our XML and JSON parsers again for this example, minus the inheritance:
class XmlParser
def parse
puts 'An instance of the XmlParser class received the parse message'
end
end
class JsonParser
def parse
puts 'An instance of the JsonParser class received the parse message'
end
end
Now we’ll build a generic parser that sends the parse
message to a parser
that it receives as an argument:
class GenericParser
def parse(parser)
parser.parse
end
end
Now we have a nice example of duck typing at work. Notice how the parse
method accepts a variable called parser
. The only thing required for this to
work is the parser
object has to respond to the parse
message and luckily
both of our parsers do that!
Let’s put together a script to see it in action:
parser = GenericParser.new
puts 'Using the XmlParser'
parser.parse(XmlParser.new)
puts 'Using the JsonParser'
parser.parse(JsonParser.new)
This script will result in the following output:
Using the XmlParser
An instance of the XmlParser class received the parse message
Using the JsonParser
An instance of the JsonParser class received the parse message
Notice that the method behaves differently depending on the object that receives it’s message. This is polymorphism!
Decorator Pattern
We can also achieve polymorphism through the use of design patterns. Let’s look at an example using the decorator pattern:
class Parser
def parse
puts 'The Parser class received the parse method'
end
end
We need to change our XmlParser
to include a constructor that accepts a
parser as an argument. The parse
method will need to be modified to send the
parse
message to the parser it receives when constructed:
class XmlParser
def initialize(parser)
@parser = parser
end
def parse
@parser.parse
puts 'An instance of the XmlParser class received the parse message'
end
end
We’ll make the same change to our JsonParser
:
class JsonParser
def initialize(parser)
@parser = parser
end
def parse
puts 'An instance of the JsonParser class received the parse message'
@parser.parse
end
end
We’ll use the decorators to create our normal XML and JSON parsers, but in the last example, we’ll do something a little different: use both decorators to achieve runtime polymorphism:
puts 'Using the XmlParser'
parser = Parser.new
XmlParser.new(parser).parse
puts 'Using the JsonParser'
JsonParser.new(parser).parse
puts 'Using both Parsers!'
JsonParser.new(XmlParser.new(parser)).parse
This script will give us the following output:
Using the XmlParser
The Parser class received the parse method
An instance of the XmlParser class received the parse message
Using the JsonParser
An instance of the JsonParser class received the parse message
The Parser class received the parse method
Using both Parsers!
An instance of the JsonParser class received the parse message
The Parser class received the parse method
An instance of the XmlParser class received the parse message
Notice how we’re able to change the results of sending the parse
message
based on the output.
A Simple Before and After
Now we will look at an intentionally simple example of how taking advantage of polymorphism can simplify our code. Let’s say we had the following classes:
class Parser
def parse(type)
puts 'The Parser class received the parse method'
if type == :xml
puts 'An instance of the XmlParser class received the parse message'
elsif type == :json
puts 'An instance of the JsonParser class received the parse message'
end
end
end
We can simplify our Parser
class by removing the branch logic thanks to simple
duck typing. In this particular example we also get the benefit of separating
concerns. Now we have our specific parsing logic encapsulated within their own
classes:
class Parser
def parse(parser)
puts 'The Parser class received the parse method'
parser.parse
end
end
class XmlParser
def parse
puts 'An instance of the XmlParser class received the parse message'
end
end
class JsonParser
def parse
puts 'An instance of the JsonParser class received the parse message'
end
end
This example demonstrates that we were able to simplify our class using polymorphism. We were also able to satisfy the Single Responsibility Principle.
In the initial version of the code the Parser class determined which parser to use and then initiated the parsing by instantiating and sending the parse message to the object. In the latter version this class only sends the parse method to kick off the process.
Polymorphism is one of the foundational elements of object oriented programming, but can also be confusing to get a grasp on. Taking the time to understand it and why it’s important is a big step towards writing more maintainable and extensible code.
What’s next
If you found this useful, you might also enjoy: