paint-brush
Welcome to the OOP pattern matching: Visitor Patternby@Pcc
1,490 reads
1,490 reads

Welcome to the OOP pattern matching: Visitor Pattern

by Luca PiccinelliAugust 17th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Pattern matching is a founding block of FP software design, and an effective way to address the need of decoupling domain objects data and behaviors. FP is much more expressive than OOP with the Visitor pattern. The original purpose of the pattern was to iterate an operation over collections of heterogeneous objects, that doesn’t share the same interface. In this case, the compiler breaks every concrete visitor, until you implement the new method in each of them. In Kotlin, this is the same as FP pattern matching.

Coin Mentioned

Mention Thumbnail
featured image - Welcome to the OOP pattern matching: Visitor Pattern
Luca Piccinelli HackerNoon profile picture

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!