DEV Community

Musale Martin
Musale Martin

Posted on

2

The Visitor Design Pattern

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

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post →

Top comments (0)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay