Go Visitor Pattern

1 minute read

The visitor pattern allows us to extend the behaviour of various objects through a common interface.

It is particularly powerful when working with the composite pattern or any kind of graph execution where we want to keep our graph traversal logic seperate from our handling logic.

The Pattern

To show how the visitor pattern works, we will extend arbitrary shapes using visitors.

For example, we may have a Shape class which:

  • accepts a Visitor
  • is implemented by Circle and Rectangle
---
title: Class diagram
---
classDiagram
  Shape <|-- Circle
  Shape <|-- Rectangle
  class Shape{
    <<Interface>>
    +Accept(Visitor)
  }
  class Circle{
    +float64 Radius
  }
  class Rectangle{
    +float64 Length
    +float64 Width
  }
  class Visitor{
    <<Interface>>
    +DoCircle(Circle)
    +DoRectangle(Rectangle)
  }

Say we define an Area visitor:

---
title: Area Visitor
---
classDiagram
  Visitor <|-- Area
  class Visitor{
    <<Interface>>
    +DoCircle(Circle)
    +DoRectangle(Rectangle)
  }
  class Area{
    <<Interface>>
    +DoCircle(Circle)
    +DoRectangle(Rectangle)
    +Calculate(Shape) float64
  }

If we call Area::Calculate we get:

sequenceDiagram
    participant Caller
    participant Area
    participant Circle
    Caller->>Area: Calculate(Circle)
    Area->>Circle: Accept(self)
    Circle->>Area: DoCircle(self)
    Area-->>Caller: result

The call to Accept and DoCircle is known as double dispatch and allows the Caller to not know that the Shape it has sent Area is infact a Circle.

Go Code

In go this looks like:

type Visitor interface {
  DoCircle(Circle)
  DoRectangle(Rectangle)
}

type Shape interface {
  Accept(Visitor)
}

type Circle struct {
  float64 Radius
}

func (c *Circle) Accept(v Visitor) {
  v.DoCircle(c)
}

type Rectangle struct {
  float64 Length
  float64 Width
}

func (r *Rectangle) Accept(v Visitor) {
  v.DoRectangle(c)
}

We are now setup to extend Circle and Rectangle shapes as much as we like.

An Area visitor will look like:

type Area struct {
  float64 result
}

func (v *Area) Calculate(s Shape) float64 {
  s.Visit(v)
  return v.result
}

func (av *Area) DoCircle(c *Circle) {
  v.result = math.Pi * math.Pow(c.Radius,2)
}

func (v *Area) DoRectangle(r *Rectangle) {
  v.result = r.Length * r.Height
}

Another example is a Circumference visitor:

type Circumference struct {
  float64 result
}

func (v *Circumference) Calculate(s Shape) float64 {
  s.Visit(v)
  return v.result
}

func (a *Circumference) DoCircle(c *Circle) {
  v.result = 2.0 * math.Pi * c.Radius
}

func (v *Circumference) DoRectangle(r *Rectangle) {
  v.result = 2.0 * (r.Length + r.Height)
}