Object-Oriented Programming: Polymorphism
- 17 February 2017
- Ruby Design
- Object-Oriented Programming: Polymorphism
Last week we discussed encapsulation and inheritance. Today we will discuss last part of basics of Object-Oriented Programming: Polymorphism.
There are couple different definitions of polymorphism. But we will stick to this one:
Polymorphism describes a pattern in object oriented programming in which classes have different functionality while sharing a common interface.
Let's consider couple examples.
Example #1
Let's imagine that we have application which uploads user's files to Dropbox, GDrive and iCloud.
We have legacy code which out colleagues created without polymoprhism.
Code looks like that:
class Dropbox
def upload_file(file)
# ...
end
end
class Gdrive
def upload(file)
# ...
end
end
class Icloud
def send(file)
# ...
end
end
class Uploader
def upload(channel, file)
case channel
when "dropbox"
Dropbox.new.upload_file(file)
when "gdrive"
Gdrive.new.upload(file)
when "icloud"
Icloud.new.send(file)
else
"Can not send file through #{channel}"
end
end
end
We have 3 classes for each storage provider. Also we have Uploader
class which knows how to send file to each provider.
Let's see disadvantages of such approach. Code of Uploader
class seems bad. Naming inside each of 3 classes it's a problem as well. All they do the same action, but have different names: Dropbox#upload_file, Gdrive#upload, Icloud#send
.
In definition of polymorphism we have this part:
classes have different functionality while sharing a common interface
If we want to get all advantages of polymorphism - we need an objects which would have the same interface. Let's start refactoring. Those 3 classes should have the same interface:
class Dropbox
def upload(file)
# ...
end
end
class Gdrive
def upload(file)
# ...
end
end
class Icloud
def upload(file)
# ...
end
end
Great, now all three classes have the same interface - they all accept message .upload(file)
.
It allows us to rewrite code of Uploader
class:
class Uploader
AVAILABLE_CHANNELS = {
"dropbox" => Dropbox,
"gdrive" => Gdrive,
"icloud" => Icloud
}.freeze
def upload(channel, file)
return "Can not send file through #{channel}" unless AVAILABLE_CHANNELS.has_key?(channel)
AVAILABLE_CHANNELS[channel].new.upload(file)
end
end
Uploader
knows which class to use for each channel of distribution (using hash AVAILABLE_CHANNELS
). It selects proper class and sends message .upload(file)
. Now it's a polymorphism, because we call method upload
without any knowledge of type receiver.
This is true because at the moment of execution this line AVAILABLE_CHANNELS[channel].new.upload(file)
doesn't know for which particular class it's going to call .new.upload(file)
. It depends on channel through which user wants to upload file.
Also, Uploader
class knows nothing about internal implementation of each class. All it knows - that each of them accepts this message .upload(file)
.
Now code looks better. Classes have the same interface and Uploader
class takes advantage of that. We refactored code and made our system more flexible using polymorphism.
Example #2
Second example will be related to reporting. Let's say that our system has number of different reports:
class SalaryReport
def rows
3
end
def expenses
19000
end
end
class SystemReport
def rows
40
end
def expenses
8000
end
end
I've removed the code which would calculate those numbers to simplify example. We have salary report and system report. All reports have the same interface: methods rows
and expenses
. Our task to create class, which would return consolidated report from those 2 reports.
As you could expect, we will use polymorphism:
class SummaryReport
def self.summary(reports)
return if reports.empty?
reports.each_with_object({ expenses: 0, rows: 0}) do |report, result|
result[:rows] += report.rows
result[:expenses] += report.expenses
end
end
end
reports = [SalaryReport.new, SystemReport.new]
SummaryReport.summary(reports) # => {:expenses=>27000, :rows=>43}
Let's go through this example: SummaryReport#summary
accepts an array of report classes. We will use that array of reports to generate consolidated report.
We go through each report in array, call rows
and expenses
and save summary into resulting hash.
We're able to use this approach, because we know that all report classes have the same inteface. SummaryReport
doesn't even know which type of classes he operates. All he should know - that he can send to each report
messages about expenses and rows number.
That code looks a bit complicated because of each_with_object
, but you can read more about it in one of my previous articles.
I don't like one part of this code. Class SummaryReport
knows about methods of each report. We can do this code even more flexible and pass into SummaryReport
fields which we would like to include into consolidated report.
class SummaryReport
def self.summary(reports, fields = [:expenses, :rows])
return if (reports.empty? || fields.empty?)
results = Hash.new(0)
reports.each do |report|
fields.each { |field| results[field] += report.public_send(field) }
end
results
end
end
reports = [SalaryReport.new, SystemReport.new]
SummaryReport.summary(reports) # => {:expenses=>27000, :rows=>43}
We have the same result, but summary
method allows us easily configure not just reports, but fields as well.
For example now we can do something like that:
reports = [SalaryReport.new]
SummaryReport.summary(reports, [:expenses]) # => {:expenses=>19000}
Pass one report and pull just expenses
field. Or get total amount of rows in two reports:
reports = [SalaryReport.new, SystemReport.new]
SummaryReport.summary(reports, [:rows]) # => {:rows=>43}
Let's consider how we achieved that flexibility:
.summary
method accepts two arguments: array of reports and fields by which we want to get total amount. By default we will use two fields: [:expenses, :rows]
.
First line it's a guard clause:
return if (reports.empty? || fields.empty?)
It returns nil
if reports
or fields
array is empty.
Then we create results
hash with default value of 0:
results = Hash.new(0)
We need that default zero to make this code working: results[:foo] += 10
.
Then we have the body of that method:
reports.each do |report|
fields.each { |field| results[field] += report.public_send(field) }
end
It goes through each report
and then through each field.
For each field we run this code:
results[field] += report.public_send(field)
Hash results
will contain total amount for each field.
public_send(:method)
method allows to call public method of object. We use that to call it dynamically using field which we want to add to report.
This code might look a bit complicated, but if you get the idea behind that - it will look really flexible and powerful.
Ok, let's recap: Polymorphism describes a pattern in object oriented programming in which classes have different functionality while sharing a common interface.
Polymorphism it's a really important part of OOP because it allows us to create flexible applications. As we've seen on examples - objects which have the same interface allows other classes to use them without any knowledge about type of internal implementation. The same interface (but different implementation) allows us to call methods dynamically.
If you have any questions or suggestions - add comments.
Thanks!