Design Patterns in Go: Visitor

Introduction

The visitor pattern is a design pattern that allows for adding new operations to a collection of objects, without, and that is important, modifying the objects themselves.

If you want to implement the Visitor pattern in Go , you can do that using interfaces and type-assertions.

First we will look at the basic structure:

We see two main functionalities:

  1. The visit functionality as implemented in the Visitor interface. This ensures that Elements can be visited.
  2. The accept functionality as implemented in the Element interface. This ensures that when an Element is visited, an operation can be performed.

Implementation in Go

Open your terminal in an empty directory and type:

go mod init github.com/visitorpattern

Now open the directory in your favourite IDE and add a main.go file. In this file, first start with the following:

package main

import "fmt"

Now define the two interfaces, i.e. the two functionalities. Since Go does not support method overloading, we need two methods in this case. This could be seen as a drawback, on the other hand: it can be quite handy to see whether a class can be or need to be visited.

type Visitor interface {
	VisitPerson(*Person)
	VisitOrganization(*Organization)
}

type Element interface {
	Accept(Visitor)
}

Now define a Person struct with the Element interface implemented:

type Person struct {
	name  string
	email string
}

func (p *Person) Accept(v Visitor) {
	v.VisitPerson(p)
}

Do the same thing for the Organization, also with the Element interface implemented:

type Organization struct {
	name    string
	address string
}

func (o *Organization) Accept(v Visitor) {
	v.VisitOrganization(o)
}

Now implement the Visitor:

type EmailVisitor struct{}

func (e *EmailVisitor) VisitPerson(p *Person) {
	fmt.Printf("Sending email to %s at %s\n", p.name, p.email)
}

func (e *EmailVisitor) VisitOrganization(o *Organization) {
	fmt.Printf("Sending mail to %s at %s\n", o.name, o.address)
}

These are the operations to be performed whenever the respective classes are visited.

Now to see it all work, we will implemented a short main program:

func main() {
	elements := []Element{
		&Person{name: "Alice", email: "alices@example.com"},
		&Organization{name: "Acme Inc.", address: "123 Main St."},
		&Person{name: "Bob", email: "bob@example.com"},
	}

	visitor := &EmailVisitor{}

	for _, element := range elements {
		element.Accept(visitor)
	}

}

A short breakdown of this code, line by line:

  1. First we define an elements array. Since both Person and Organization implementg the Element interface, this kan be an []Element array.
  2. We set up our visitors.
  3. Now we iterate over the elements array and visit each element. If the element is of type Person for example, the visitPerson of the visitor is called, and vice versa for the Organization type where visitOrganization is being called.

Conclusion

As you can see, the implementation of this pattern is both elegant and painless. That is painless for new implementation, I realize that may not be the case for existing codebases.

Leave a Reply

Your email address will not be published. Required fields are marked *