Object-Oriented Programming: Polymorphism

Subscribe to receive new articles. No spam. Quality content.

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.keys.include?(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!

Subscribe to receive new articles. No spam. Quality content.

Comments