The Visitor Design Pattern
Musale Martin
Posted on May 12, 2020
Using an example of cocktails. Say we have a Cocktail
that we can use to "make" different kinds of cocktails, like Mojito
and Daiquiri
. We can have a Cocktail
interface that defines Serve
and Drink
methods for a cocktail:
type Cocktail interface{
Serve()
Drink()
}
You can make the Mojito
and Daiquiri
implement the Cocktail
interface methods like this:
type Mojito struct{}
func(m Mojito) Serve(){}
func(m Mojito) Drink(){}
type Daiquiri struct{}
func(m Daiquiri) Serve(){}
func(m Daiquiri) Drink(){}
The challenge
Now, we want to be able to define new Cocktail
operations without having to add the new methods on each existing cocktail. For the above implementation, we'll have to add the method in the Cocktail
interface and then implement the method for each of the cocktails that we want to implement the method.
The visitor pattern will allow you to operate on a Cocktail
so that you can provide a Cocktail
that easily conforms to all the cocktail operations. It makes it easier to add new ways to work on a cocktail without getting to make the change on every cocktail.
The Visitor Pattern
To achieve this, we implement a new interface; CocktailVisitor
that defines the visitor operations on a cocktail.
type CocktailVisitor interface{
visitMojito(c Mojito)
visitDaiquiri(c Daiquiri)
}
Next, we need to "route" the correct cocktail to the correct method on the visitor by changing the Cocktail
interface.
type Cocktail interface{
accept(v CocktailVisitor)
}
With that, we are now able to make the Cocktail
interface to have a method accept
that takes in a CocktailVisitor
or any object "that is" a CocktailVisitor
.
Finally, we make the cocktails:
type Mojito struct{}
func (m Mojito) accept(v CocktailVisitor){
v.visitMojito(m)
}
type Daiquiri struct{}
func (d Daiquiri) accept(v CocktailVisitor){
v.visitDaiquiri(d)
}
Now, if you need to add a new cocktail like a Margharita
, you just have to implement its visitor and add it to the CocktailVisitor
.
Usage
You will need to define the operation you want to accomplish. We want to do a DrinkVisit
operation:
type DrinkVisit struct{}
func (DrinkVisit) visitMojito(m Mojito) {
fmt.Println("Drinking mojitos")
}
func (DrinkVisit) visitDaiquiri(d Daiquiri) {
fmt.Println("Drinking daiquiris")
}
Then we do the magic like:
func main() {
visitor := DrinkVisit{}
mojito := &Mojito{}
daiquiri := &Daiquiri{}
mojito.accept(visitor)
daiquiri.accept(visitor)
}
A working play example can be found here
Conclusion
The visitor pattern is a common pattern in language interpreters that allows the interpreter to work on various expressions by defining operations on them. Typically, the accept
method will return an interface{}
or a value. That is well beyond the scope of this write-up.
Initially posted at musale.github.io
Posted on May 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.