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.
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.
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
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).
- Do the Video Store Kata, and consider how you could build taking advantage of polymorphism.