OOP Pattern Matching: Visitor Pattern
Using the visitor pattern as a routing point (with Kotlin examples)
Functional programming gained a big momentum in the IT field; many things come and go, but FP is not one of them. It is much more expressive than OOP.
I started digging into it a few years ago, during the LambdaConf in Bologna, and the more insight I get, the more I love FP.
In June, I went to an applied functional programming workshop and the teacher went deep into algebraic data types and pattern matching. I also finally understood what a monad is, but this is another story.
I used to think of pattern matching as an interesting way to destructure lists. Now, I know that it is a founding block of FP software design and an effective way to address the need for decoupling domain objects’ data and behaviors.
Let’s see an example!
ERP Localized by Country
Let’s say that we want to write ERP software, to compete in the markets of two different countries: Italy and Germany. We are going to write many CRUD operations in many domain objects. Nothing challenging here.
But, what about modeling country-specific business rules, in different data structures, inside the same functionality flow?
For example, both countries have articles in their databases and you definitely want to perform searches on those. But, search rules and fetched data can be totally different.
As the data structures are different, we can’t just have the domain entity
Article — we need at least an
ItalianArticle and a
Consider that data stores could also differ by structure.
The FP Way
Let’s see a Scala implementation.
Write a sum type
Then, we specialize it as the product types
Also, we can expect that when we search for something, sometimes we find nothing. So, we are going to also consider
Every time we receive an
Article back from somewhere, we are going to pattern match on it. Inside the match, we gain access to its specialized data.
Isn’t It a Switch?
At first sight, it could look like a classic procedural “switch”, followed by a down-casting.
There is an important difference: the compiler is aware of whether we are matching every type of
Article or not. If, for example, we forget to match
Then, by default, the
sbt compiler is going to raise a warning.
As the compiler knows that something is wrong, we can make it raise an error instead of a warning.
New Requirements: Spain
At this point, something totally unexpected happens! The business comes to us with a new requirement: we also need to distribute in Spain.
Actually, we were prepared, so let’s add
EspArticle to our
Article sum type.
Now, we need to add Spanish business logic.
With a normal switch, it would be a pain to search for every place where we implemented country-specific business rules.
Instead, with pattern matching, the compiler tells us where we have to intervene.
Is It Possible With OOP?
Yes of course, with the visitor pattern!
If you don’t know about the visitor pattern, have a look at this Wikipedia page. If you don’t know about the Gang of Four (who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software), have a read of it.
The visitor pattern has been known for a long time as an anti-pattern. When you add a new item type, you are going to add a new method to the visitor interface.
In this case, the compiler breaks every concrete visitor, until you implement the new method in each of them. This is one of the reasons for an anti-pattern.
Actually, for our concerns, this “issue” looks exactly like what we are searching for!
Here is a Kotlin implementation of our sum types:
You’ll notice that, instead of using the names “accept” and “Visitor”, I prefer to think about “data that are applied to a data consumer”. (I’m looking for better names, so if you have suggestions, let me know.)
And, here, we can implement country-specific business rules.
The semantic of this example is totally the same as FP pattern matching.
When we add the new country, the compiler is going to break everything, until we don’t implement the new
fun use(article: SpaArticle) in every
The original purpose of the visitor pattern was to iterate an operation over collections of heterogeneous objects, which don’t share the same interface and data types.
In this article, I proposed to use it, instead, as a routing point. It is useful when you end up enumerating your domain entities and need to set a localized domain context.
Enumerating domain entities in methods is argued as being an issue. In this use case, it is the key feature of the pattern.
I also demonstrated that this usage is semantically equal to FP pattern matching.
Thank you for reading!