Polymorphism
One fundamental feature of object-oriented programming is polymorphism.
Polymorphism is a fancy term to describe how multiple Concrete types can conform to an Abstract Interface, and be used to switch out behaviours at runtime.
Let’s explore this with an example.
Consider the following code:
class AccountingDocument
def initialize(type:, total:)
@type = type
@total = total
end
def get_journal
case @type
when :sale
[
:trade_debtors, -@total,
:revenue, @total
]
when :purchase
[
:trade_creditors, @total,
:loss, -@total
]
when :expense
[
:employee_expenses, @total,
:bank_account, -@total
]
when :draft
nil
else
end
end
end
Where an example usage looks like:
AccountingDocument.new(type: :purchase, total: '100.00')
This code is not Polymorphic.
The problem
It violates the Open-Closed Principle. Potentially, also the Single Responsibility Principle too.
Every time we need to introduce a new type of AccountingDocument
, we’re going to need to modify this class.
What’s more, it will be a magnet for an ever growing number of journal representations, for an ever growing number of those document types.
An alternative
Instead, we can use polymorphism:
class AccountingDocument
def initialize(journal_disposition:, total:)
@journal_disposition = journal_disposition
@total = total
end
def get_journal
@journal_disposition.get_journal(@total)
end
end
The above class can be said to be “Open for Extension” and “Closed for Modification”.
We can then define classes for each “JournalDisposition”
class SaleJournalDisposition
def get_journal(total)
[
:trade_debtors, -@total,
:revenue, @total
]
end
end
class PurchaseJournalDisposition
def get_journal(total)
[
:trade_creditors, @total,
:revenue, -@total
]
end
end
class ExpenseJournalDisposition
def get_journal(total)
[
:employee_expenses, total,
:bank_account, -@total
]
end
end
class NoJournalDisposition
def get_journal(_); end
end
But wait! We still need something which can convert data
into behaviour
If we want to reconstruct polymorphic behaviours from data stored in a database,
we need a mapping between the data
: type (a string) and the behaviour
: the appropriate Ruby object.
class JournalDispositionFactory
def create(type)
case type
when :sale
SaleJournalDisposition.new
when :purchase
PurchaseJournalDisposition.new
when :expense
ExpenseJournalDisposition.new
else
NoJournalDisposition.new
end
end
end
As an example usage:
journal_disposition_factory = JournalDispositionFactory.new
AccountingDocument.new(journal_disposition: journal_disposition_factory.create(:sale), total: '56.23')
But wait, that’s more code!
Sure - there is more code after our refactoring.
However, software design is more than just reducing lines of code.
- Whenever we change a file, we risk breaking other things in the same file.
- Whenever we change a file that someone else is working on, we risk a merge.
- The more things a file does, the harder it is to understand.
Instead, it is desirable to break code apart into lego bricks. (We will cover this in more detail later).
Exercises
- Do the Video Store Kata, and consider how you could build taking advantage of polymorphism.