Refactoring in Clojure: Implementing Polymorphism with Multimethods
Recently, I took a stab at completing the Gilded Rose kata in Clojure. The kata was new and different for me, because you begin with a section of code already written. The challenge isn’t to quickly implement a simple algorith; it’s to thoughtfully decompose an overly-complicated wreck.
One of the main challenges to tackle in Gilded Rose is decomposing a massive set of nested conditionals into a more readable and extensible algorithm. The naive first step is to simply extract methods from each branch of the outermost conditions, but you’re still left with code that violates the open-closed principle. New additions will require you to add another branch.
This problem is amplified by the ultimate task of the kata: to add a new item to the store’s inventory. You just can’t feel good about completing that last step unless you’ve found some way to render the code open for extension but closed for modification.
So the question that presents itself is how to do so. While an object-oriented language would rely on polymorphism (where each item type would carry its own implementation of the method that updates its quality), Clojure’s lack of classes and objects would seem to place such an approach out of bounds.
Luckily, that’s not the case. Enter multimethods.
Implementing Polymorphism With Multimethods
Multimethods entail a collection of implementations of a particular function where one is selected for application depending on the nature of the argument(s) passed in. They’re defined with one definition that explains how to determine which implementation to select, then a variety of implementations for different arguments.
You can see that the first definition uses defmulti
to define a dispatching function that returns a dispatching value. We take a look at the value associated with the key :num
in the num-map
passed in1.
Next, we define a set of methods with dispatch values. If the dispatching function returns 1
, Clojure knows to choose the method with the dispatch value of 1
. The same logic applies for 2
, and then we create a default implementation for if neither of the above implementation match the dispatching value.
There are several other ways to implement polymorphism in Clojure2, but the multimethod is a great way to tackle the particular problem posed by Gilded Rose. Doing so allows us to transform this:
Into this:
There are still improvements to be made, for sure. But we’ve now decomposed a massive beast of nested conditionals into a manageable collection of bite-sized chunks. And, best of all, we’re now satisfying the open-closed principle; adding a new item type requires - at most - that we add another implementation of adjust-quality
.
Conclusion
It’s important not to overuse Clojure’s sophisticated facilities for implementing polymorphism, since doing so can provide a crutch to the object-oriented programmer that enables you to dodge more idiomatically functional ways of tackling problems. But when a huge method just screams out for decomposition, resources like Clojure’s multimethod can make it easier to accomplish your objectives in a way that yields extensible code.
-
We could actually just put
:num
for our dispatching function - Clojure knows to treat a keyword as a function. I’ve used the more verbose form here for clarity as to what exactly is going on. The verbose form can be useful when you want to identify the correct implementation based on some more nuanced evaluation of your argument(s). ↩ -
Check out this and this for further information on using functions, protocols, and records to implement the best form of polymorphism for your particular problem. ↩