When I first came across the concept of Polymorphism in Object-Oriented Programming (OOP), it wasn’t obvious to me why it was important. The top search results generally explain what it is and how to use it, but handwave the why and treat it as yet-another-language-feature.
Then I came across this statement in a blog post by Robert C. Martin:
…the thing that truly differentiates OO programs from non-OO programs is polymorphism.
Whoa! Now he didn’t mean that encapsulation and other “OOP concepts” were unimportant - his point was that they are achievable in “non-OOP” as well, but polymorphism was not.
If polymorphism is such a defining aspect of OOP, could we build a stronger intuition for why it’s important?
Since polymorphism has different meanings depending on context1, let’s align on what I meant by “Polymorphism in OOP”. In the earlier blog post, Martin explains it as different objects being able to accept the same message, implementing their own behaviour. I’ll paraphase his example:
We don’t actually know what
some_object is! Nor does it actually matter. Many different implementations could replace
some_object, and so long as they have the same interface (i.e. have the method
#do_something), the program would still run.
In another blog post, Martin says:
There really is only one benefit to Polymorphism; but it’s a big one. It is the inversion of source code and run time dependencies.
It wasn’t initially obvious to me what that meant, so let’s concretise this with an example: caching in a web framework like Rails. There are multiple options out there for caches - for example, Rails supports a
MemCacheStore and a
RedisCacheStore. For the sake of illustration, imagine that the caches all had their own interfaces:
def example_method # do stuff if Rails.cache.is_a? NaiveFileStore data = Rails.cache.search_for(key) elsif Rails.cache.is_a? NaiveRedisCacheStore data = Rails.cache.get(key) end # do more stuff end
example_method has a direct dependency on
NaiveRedisCacheStore. If their interfaces change, or new kinds of store need to be supported, it will require changes in
Earlier, Martin talked about an inversion of dependencies (the “D” in SOLID) - practically, this means
example_method and the different cache stores agree on an interface, and both depend on it. This makes all cache stores polymorphic! In Rails, all cache stores implement a
#fetch method, so the above code becomes:
def example_method # do stuff data = Rails.cache.fetch(key) # do more stuff end
The code in
example_method is more concise now, but that’s a side benefit; what’s important is it’s shielded from knowing which cache store is used, and how the store works. It just knows that some cache store exists (
Rails.cache), and the cache store has agreed to implement a
Martin describes this as a “plugin architecture”:
This inversion allows the called module to act like a plugin. Indeed, this is how all plugins work… Plugin architectures are very robust because stable high value business rules can be kept from depending upon volatile low value modules such as user interfaces and databases.
With the new setup, the polymorphic cache stores can be swapped for each other without changing the code in
example_method. New kinds of cache stores can also be supported by creating classes that implement
#fetch and other methods as specified by ActiveSupport::Cache::Store.
In short, polymorphism makes it easier to extend or change aspects of our programs, without a rippling of changes throughout the entire program. (How do we determine the “aspects” to split our programs by? A good read would be Parnas’ classic 1972 paper.)
While our cache example does rely on inheritance to make the polymorphic group explicit, inheritance is not a prerequisite. For example, each cache could be a completely unrelated class, and it still would work in Ruby as long as they had a
#fetch method (this is also called “duck-typing”).
You may have spotted that Martin talks about inversion of “source code” in addition to runtime dependencies. I understand this has greater implications in compiled programs, but am not familiar enough to expound on it. Do leave a comment if you have an example!
There are many other neat examples relating to polymorphism. Here’s some I thought of:
- Martin Fowler’s refactoring book has a “Replace Conditional with Polymorphism” refactoring, which I think was neatly illustrated in this Sandi Metz talk.
- Many Behavioural Design Patterns rely on polymorphism, for example the Strategy, Command, Visitor etc.
- The NullObject is a neat way to support a null value or no-ops when your code relies on a Polymorphic interface. (Note that while Rails has a NullCache, it doesn’t solve the same issue that the pattern is meant to.)
- Ruby-specific example of a “Plugin Architecture”: Rubocop allows you to add your own custom cops without changing the library’s source code, because all cops are polymorphic!
- The definition I used for polymorphism is actually somewhat narrow, but seemed like a relatively common understanding in the context of OOP. The Wikipedia article on polymorphism shows a lot more breadth in the topic, and I think what is described above is known as single, dynamic dispatch. There’s some really good discussion on this in the Reddit thread for this post.↩