Design Pattern: Visitor Pattern in Go

Visitor pattern moves some logic of certain types of objects to an external entity(named the visitor), and that external entity/visitor is responsible for performing operations on the object. This pattern separates the logic to work with certain types of objects.

This article demonstrates Visitor pattern implementations in GoLang. Check the following examples.

Implementation

Here is a simple example of the Visitor pattern implementation in GoLang-

// Visitor pattern implementation in Go

package main

import "fmt"

type IElement interface {
	visit(visitor IVisitor)
}

// First element
type Element1 struct {
	content string
}

func NewElement1(content string) (element *Element1) {
	element = &Element1{}
	element.content = content
	return
}

func (element *Element1) getContent() string {
	return element.content
}

func (element *Element1) visit(visitor IVisitor) {
	visitor.addContent(element)
}

// Second element
type Element2 struct {
	content   string
	addMargin bool
}

func NewElement2(content string, addMarring bool) (element *Element2) {
	element = &Element2{}
	element.content = content
	element.addMargin = addMarring
	return
}

func (element *Element2) getContent() string {
	return element.content
}

func (element *Element2) hasMargin() bool {
	return element.addMargin
}

func (element *Element2) visit(visitor IVisitor) {
	visitor.addContentWithMargin(element)
}

// Visitor interface
type IVisitor interface {
	addContent(element *Element1)
	addContentWithMargin(element *Element2)
}

type Visitor struct {
	Output string
}

func NewVisitor() (visitor *Visitor) {
	visitor = &Visitor{}
	return
}

func (visitor *Visitor) addContent(element *Element1) {
	visitor.Output += element.getContent()
}

func (visitor *Visitor) addContentWithMargin(element *Element2) {
	if element.hasMargin() {
		visitor.Output += "[margin]" + element.getContent() + "[/margin]"
	} else {
		visitor.Output += element.getContent()
	}
}

// Demo
func main() {
	// list of elements
	elements := []IElement{
		NewElement1("Element1: first element"),
		NewElement2("Element2: second element without margin", false),
		NewElement2("Element2: third element with margin", true),
		NewElement1("Element1: last element"),
	}

	visitor := NewVisitor()

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

	fmt.Println(visitor.Output)
}

Output will be as below-

Element1: first element
Element2: second element without margin
[margin]Element2: third element with margin[/margin]
Element1: last element

Examples

Here are a few examples of Visitor pattern implements in Golang-

Example #1: Hosting Cost Calculator

Let’s consider an example of a hosting service, and calculate the total cost of the hosting service used using the visitor pattern.

Service Interface

// service.go

package main

type Service interface {
	Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64
}

Compute Service Struct

// compute_service.go

package main

type ComputeService struct {
	price    float64
	quantity int
}

func NewComputeService(quantity int) (compute *ComputeService) {
	compute = &ComputeService{}
	compute.price = 10.50
	compute.quantity = quantity
	return
}

func (compute *ComputeService) GetPrice() float64 {
	return compute.price
}

func (compute *ComputeService) GetQuantity() int {
	return compute.quantity
}

func (compute *ComputeService) Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64 {
	return hostingCalculatorVisitor.ComputeVisit(compute)
}

Database Service Struct

// database_service.go

package main

type DatabaseService struct {
	price         float64
	backPrice     float64
	quantity      int
	backupEnabled bool
}

func NewDatabaseService(quantity int) (databaseService *DatabaseService) {
	databaseService = &DatabaseService{}
	databaseService.price = 100.00
	databaseService.backPrice = 30.00
	databaseService.backupEnabled = false
	databaseService.quantity = quantity
	return
}

func NewDatabaseService2(quantity int, backupEnabled bool) (databaseService *DatabaseService) {
	databaseService = &DatabaseService{}
	databaseService.price = 100.00
	databaseService.backPrice = 30.00
	databaseService.quantity = quantity
	databaseService.backupEnabled = backupEnabled
	return
}

func (databaseService *DatabaseService) GetBackPrice() float64 {
	return databaseService.backPrice
}

func (databaseService *DatabaseService) GetPrice() float64 {
	return databaseService.price
}

func (databaseService *DatabaseService) GetQuantity() int {
	return databaseService.quantity
}

func (databaseService *DatabaseService) IsBackupEnabled() bool {
	return databaseService.backupEnabled
}

func (databaseService *DatabaseService) Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64 {
	return hostingCalculatorVisitor.DatabaseVisit(databaseService)
}

File Storage Service Struct

// file_storage_service.go

package main

type FileStorageService struct {
	pricePerGB float64
	quantity   int
}

func NewFileStorageService(quantity int) (fileStorageService *FileStorageService) {
	fileStorageService = &FileStorageService{}
	fileStorageService.pricePerGB = 1.70
	fileStorageService.quantity = quantity
	return
}

func (fileStorageService *FileStorageService) GetPricePerGB() float64 {
	return fileStorageService.pricePerGB
}

func (fileStorageService *FileStorageService) GetQuantity() int {
	return fileStorageService.quantity
}

func (fileStorageService *FileStorageService) Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64 {
	return hostingCalculatorVisitor.FileVisit(fileStorageService)
}

Serverless Service Struct

// serverless_service.go

package main

type ServerlessService struct {
	hourlyPrice float64
	totalHours  int
}

func NewServerlessService(totalHours int) (serverlessService *ServerlessService) {
	serverlessService = &ServerlessService{}
	serverlessService.hourlyPrice = 0.32
	serverlessService.totalHours = totalHours
	return
}

func (serverlessService *ServerlessService) GetHourlyPrice() float64 {
	return serverlessService.hourlyPrice
}

func (serverlessService *ServerlessService) GetTotalHours() int {
	return serverlessService.totalHours
}

func (serverlessService *ServerlessService) Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64 {
	return hostingCalculatorVisitor.ServerlessVisit(serverlessService)
}

Container Service Struct

// container_servie.go

package main

type ContainerService struct {
	price    float64
	quantity int
}

func NewContainerService(quantity int) (containerService *ContainerService) {
	containerService = &ContainerService{}
	containerService.price = 5.60
	containerService.quantity = quantity
	return
}

func (containerService *ContainerService) GetPrice() float64 {
	return containerService.price
}

func (containerService *ContainerService) GetQuantity() int {
	return containerService.quantity
}

func (containerService *ContainerService) Accept(hostingCalculatorVisitor HostingCalculatorVisitor) float64 {
	return hostingCalculatorVisitor.ContainerVisit(containerService)
}

Visitor Interface

// hosting_calculator_visitor.go

package main

type HostingCalculatorVisitor interface {
	ComputeVisit(computeService *ComputeService) float64
	ContainerVisit(containerService *ContainerService) float64
	DatabaseVisit(databaseService *DatabaseService) float64
	FileVisit(fileStorageService *FileStorageService) float64
	ServerlessVisit(serverlessService *ServerlessService) float64
}

Visitor Interface Implementation

// hosting_calculator_visitor_impl.go

package main

type HostingCalculatorVisitorImpl struct{}

func NewHostingCalculatorVisitorImpl() (hcvi *HostingCalculatorVisitorImpl) {
	hcvi = &HostingCalculatorVisitorImpl{}
	return
}
func (hcvi *HostingCalculatorVisitorImpl) ComputeVisit(computeService *ComputeService) float64 {
	return computeService.GetPrice() * float64(computeService.GetQuantity())
}

func (hcvi *HostingCalculatorVisitorImpl) ContainerVisit(containerService *ContainerService) float64 {
	return containerService.GetPrice() * float64(containerService.GetQuantity())
}

func (hcvi *HostingCalculatorVisitorImpl) DatabaseVisit(databaseService *DatabaseService) float64 {
	serviceCost := databaseService.GetPrice() * float64(databaseService.GetQuantity())

	backupCost := 0.00
	if databaseService.IsBackupEnabled() {
		backupCost = databaseService.GetBackPrice() * float64(databaseService.GetQuantity())
	}
	return serviceCost + backupCost
}

func (hcvi *HostingCalculatorVisitorImpl) FileVisit(fileStorageService *FileStorageService) float64 {
	return fileStorageService.GetPricePerGB() * float64(fileStorageService.GetQuantity())
}

func (hcvi *HostingCalculatorVisitorImpl) ServerlessVisit(serverlessService *ServerlessService) float64 {
	return serverlessService.GetHourlyPrice() * serverlessService.GetHourlyPrice()
}

Demo

// main.go

package main

import "fmt"

func main() {
	usedServices := []Service{
		NewComputeService(3),
		NewDatabaseService2(3, true),
		NewFileStorageService(120),
		NewServerlessService(720),
		NewContainerService(2),
	}
	totalCost := calculateHostingCost(usedServices)

	fmt.Printf("Total cost of hosting is: %f", totalCost)
}

func calculateHostingCost(services []Service) float64 {
	hostingCalculatorVisitorImpl := NewHostingCalculatorVisitorImpl()
	totalCost := 0.00

	for _, service := range services {
		totalCost += service.Accept(hostingCalculatorVisitorImpl)
	}
	return totalCost
}

Output

Total cost of hosting is: 636.802400

Example #2: UI Elements

Here is another example of visitor pattern. We are printing some UI elements(dummy) here, to demonstrate how can this pattern be used to easily print UI elements.

UI Element Interface

// ui_element.go

package main

type UIElement interface {
	AppendElement(vistor Visitor)
}

// This is not directly relevent to the visitor pattern
// This is used to match certain param type in the implementation
type IWrapperElement interface {
	GetText() string
	GetWrapper() string
}

Text Element Struct

// text_element.go

package main

type TextElement struct {
	text string
}

func NewTextElement(text string) (textElement *TextElement) {
	textElement = &TextElement{}
	textElement.text = text
	return
}

func (textElement *TextElement) GetText() string {
	return textElement.text
}

func (textElement *TextElement) AppendElement(visitor Visitor) {
	visitor.AppendContent(textElement)
}

Wrap Element Struct

// wrap_element.go

package main

type WrapElement struct {
	text    string
	wrapper string
}

func NewWrapElement(text string, wrapper string) (wrapElement *WrapElement) {
	wrapElement = &WrapElement{}
	wrapElement.text = text
	wrapElement.wrapper = wrapper
	return
}

func (wrapElement *WrapElement) GetText() string {
	return wrapElement.text
}

func (wrapElement *WrapElement) GetWrapper() string {
	return wrapElement.wrapper
}

func (wrapElement *WrapElement) AppendElement(visitor Visitor) {
	visitor.AppendContentWithWrapper(wrapElement)
}

Head Element Struct

// head_element.go

package main

type HeadElement struct {
	wrapper string
	text    string
}

func NewHeadElement(text string) (headElement *HeadElement) {
	headElement = &HeadElement{}
	headElement.wrapper = "h1"
	headElement.text = text
	return
}

func (headElement *HeadElement) GetText() string {
	return headElement.text
}

func (headElement *HeadElement) GetWrapper() string {
	return headElement.wrapper
}

func (headElement *HeadElement) AppendElement(visitor Visitor) {
	visitor.AppendContentWithWrapper(headElement)
}

List Element Struct

// list_element.go

package main

type ListElement struct {
	lines []string
}

func NewListElement(lines []string) (listElement *ListElement) {
	listElement = &ListElement{}
	listElement.lines = lines
	return
}

func (listElement *ListElement) GetListItems() []string {
	return listElement.lines
}

func (listElement *ListElement) AppendElement(visitor Visitor) {
	visitor.AppendList(listElement)
}

Visitor Interface

// visitor.go

package main

type Visitor interface {
	AppendContent(textElement *TextElement)
	AppendContentWithWrapper(wrapElement IWrapperElement)
	AppendList(listElement *ListElement)
}

Visitor Interface Implementation

// element_visitor

package main

import "fmt"

type ElementVisitor struct {
	output string
}

func NewElementVisitor() (elementVsitor *ElementVisitor) {
	elementVsitor = &ElementVisitor{}
	return
}

func (elementVisitor *ElementVisitor) AppendContent(textElement *TextElement) {
	elementVisitor.output += textElement.text
}

func (elementVisitor *ElementVisitor) AppendContentWithWrapper(wrapElement IWrapperElement) {
	elementVisitor.output += fmt.Sprintf("[%s]%s[/%s]", wrapElement.GetWrapper(), wrapElement.GetText(), wrapElement.GetWrapper())
}

func (elementVisitor *ElementVisitor) AppendList(listElement *ListElement) {
	elementVisitor.output += "[ul]"

	for _, item := range listElement.GetListItems() {
		elementVisitor.output += fmt.Sprintf("[li]%s[/li]", item)
	}

	elementVisitor.output += "[/ul]"
}

Demo

// main.go

package main

import "fmt"

func main() {
	// set the list of elements we want print
	uiElements := []UIElement{
		NewHeadElement("My Heading"),
		NewTextElement("First line of text"),
		NewListElement([]string{"abc", "def", "ghi", "jkl"}),
		NewWrapElement("Content wrapped with div", "div"),
		NewTextElement("Last line of text"),
	}

	visitor := NewElementVisitor()

	for _, element := range uiElements {
		element.AppendElement(visitor)
	}

	// let"s check the output
	fmt.Println(visitor.output)
}

Output

[h1]My Heading[/h1]

First line of text

[ul]
    [li]abc[/li]
    [li]def[/li]
    [li]ghi[/li]
    [li]jkl[/li]
[/ul]

[div]Content wrapped with div[/div]

Last line of text

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check Visitor pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.