Visitor pattern is dead. Long live to Visitor pattern. (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 dig into it a few years ago, during LambaConf in Bologna and the more insight i get, the more i love FP. In June i went to this workshop and, the teacher went deep on algebraic data types and Pattern matching on them. I also finally understood what a Monad is đ, but this is another story đ. I used to think about 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 of decoupling domain objects data and behaviors.
Letâs see an example!
ERP localized by Country
Letâs say that we want to write an ERP software, to compete into markets of two different countries; Italy and Germany. We are going to write many CRUD operations on many domain objects. Nothing challenging here.Â
But what about modeling country specific business rules, on different data structures, inside the same functionality flow?
For example, both of the countries have articles in their databases and for sure you want to do searches on them. But search rules and fetched data could be totally different.
As data structures are different, we canât just have the domain entity âArticleâ: we need at least an âItalianArticleâ and a âGermanArticleâ. Consider that data stores could differ also by structure.
The FPÂ way
Letâs see a Scala implementation: write a sum typeÂ
Article
. Then we specialize it as the product types ItalianArticleÂ
and GermanArticle
. Also, we expect that when we search for something, sometimes we found nothing. So, we are going to consider also ArticleNotFound
.sealed trait Article
case class ItaArticle(id: Int, itaData1: String, itaData2: Int) extends Article
case class DeuArticle(id: Int, deuData1: Int, deuData2: Int) extends Article
case class ArticleNotFound() extends Article
Every time that 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
def doSomething(article: Article) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
case nf: ArticleNotFound => print("None")
}
}
Isnât it a Switch?
At a first sight, it could look like a classic procedural âswitchâ, followed by a down-casting. There is an important difference: the compiler is aware if we are matching every type of Article or not. If, for example, we forget to match onÂ
ArticleNotFound
def doSomething(article: Article) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
}
}
then by default 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! Business comes to us with a new requirement đ±: we need to distribute also in Spain.
Actually we were prepared, so letâs addÂ
SpaArticle
Â
to our Article
sum type.sealed trait Article
case class ItaArticle(id: Int, itaData1: String, itaData2: Int) extends Article
case class DeuArticle(id: Int, deuData1: Int, deuData2: Int) extends Article
case class SpaArticle(id: Int, spaData1: Float, spaData2: String) extends Article
case class ArticleNotFound() extends Article
Now we need to add Spanish business logic.
With a normal switch, it would be a pain to search for every places where we implemented country specific business rules.
With pattern matching instead, the compiler tell us where we have to intervene.
Is it possible with OOP?
Yes of course, with the Visitor pattern!
If you donât know about Visitor pattern, have a look at here
If you donât know about the Gang of four have a look at here.
Visitor pattern has been known for a long time as an anti-pattern. When you add a new item type, then you are going also 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 to be an anti-pattern.
Actually, for our concerns, this âissueâ looks like exactly what we are searching for!
Kotlin example
Here it follows a Kotlin implementation of our sum types:
interface Article{
fun applyTo(consumer: ArticleConsumer)
}
class ItaArticle(val itaData1: String, val itaData2: Int) : Article {
override fun applyTo(consumer: ArticleConsumer) {
consumer.use(this)
}
}
class DeuArticle(val deuData1: String) : Article {
override fun applyTo(consumer: ArticleConsumer) {
consumer.use(this)
}
}
You can notice that instead of using the âacceptâ and âVisitorâ naming, i prefer to think about âdata that are applied to a data consumerâ. (Iâm searching for a better naming, so if you have suggestions, please leave a comment! Thanks)
And here we can implement country specific business rules
interface ArticleConsumer {
fun use(article: ItaArticle)
fun use(article: DeuArticle)
fun use(article: ArticleNotFound)
}
fun useAnArticle(article: Article) : String {
var x = ""
article.applyTo(object : ArticleConsumer {
override fun use(article: ItaArticle) {
x = article.itaData1
}
override fun use(article: DeuArticle) {
x = article.deuData1
}
override fun use(article: ArticleNotFound) {
x = "not found"
}
})
return x
}
The semantic of this example is totally the same as of FP pattern matching. When we are going to add the new country, then the compiler is going to break everything, until we donât implement the newÂ
fun use(article: SpaArticle)
 in every ArticleConsumer
.Conclusions
The original purpose of the Visitor pattern was to iterate an operation over collections of heterogeneous objects, that doesnâ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.
Please leave your opinion and feedback in comments.
Thank you for reading!