Visitor Pattern
Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code.
Golang Implementation
Example: Opearte on different shapes
The Visitor pattern lets you add behavior to a struct without actually modifying the struct. Let’s say you are the maintainer of a lib which has different shape structs such as:
- Square
- Circle
- Triangle
Each of the above shape structs implements the common shape interface.
Once people in your company started to use your awesome lib, you got flooded with feature requests. Let’s review one of the simplest ones: a team requested you to add the getArea behavior to the shape structs.
There are many options to solve this problem.
The first option that comes to the mind is to add the getArea method directly into the shape interface and then implement it in each shape struct. This seems like a go-to solution, but it comes at a cost. As the maintainer of the library, you don’t want to risk breaking your precious code each time someone asks for another behavior. Still, you do want other teams to extend your library somehow.
The second option is that the team requesting the feature can implement the behavior themselves. However, this is not always possible, as this behavior may depend on the private code.
The third option is to solve the above problem using the Visitor pattern. We start by defining a visitor interface like this:
type visitor interface {
visitForSquare(square)
visitForCircle(circle)
visitForTriangle(triangle)
}
The functions visitForSquare(square), visitForCircle(circle), visitForTriangle(triangle) will let us add functionality to squares, circles and triangles respectively.
Wondering why can’t we have a single method visit(shape) in the visitor interface? The reason is that the Go language doesn’t support method overloading, so you can’t have methods with the same names but different parameters.
Now, the second important part is adding the accept method to the shape interface.
func accept(v visitor)
All of the shape structs need to define this method, similarly to this:
func (obj *square) accept(v visitor){
v.visitForSquare(obj)
}
Wait a second, didn’t I just mention that we don’t want to modify our existing shape structs? Unfortunately, yes, when using the Visitor pattern, we do have to alter our shape structs. But this modification will only be done once.
In case adding any other behaviors such as getNumSides, getMiddleCoordinates, we will use the same accept(v visitor) function without any further changes to the shape structs.
In the end, the shape structs just need to be modified once, and all future requests for different behaviors could be handled using the same accept function. If the team requests the getArea behavior, we can simply define the concrete implementation of the visitor interface and write the area calculation logic in that concrete implementation.
shape.go
package main
type Shape interface {
getType() string
accept(Visitor)
}
square.go
package main
type Square struct {
side int
}
func (s *Square) accept(v Visitor) {
v.visitForSquare(s)
}
func (s *Square) getType() string {
return "Square"
}
circle.go
package main
type Circle struct {
radius int
}
func (c *Circle) accept(v Visitor) {
v.visitForCircle(c)
}
func (c *Circle) getType() string {
return "Circle"
}
rectangle.go
package main
type Rectangle struct {
l int
b int
}
func (t *Rectangle) accept(v Visitor) {
v.visitForrectangle(t)
}
func (t *Rectangle) getType() string {
return "rectangle"
}
visitor.go
package main
type Visitor interface {
visitForSquare(*Square)
visitForCircle(*Circle)
visitForrectangle(*Rectangle)
}
areaCalculator.go
package main
import (
"fmt"
)
type AreaCalculator struct {
area int
}
func (a *AreaCalculator) visitForSquare(s *Square) {
// Calculate area for square.
// Then assign in to the area instance variable.
fmt.Println("Calculating area for square")
}
func (a *AreaCalculator) visitForCircle(s *Circle) {
fmt.Println("Calculating area for circle")
}
func (a *AreaCalculator) visitForrectangle(s *Rectangle) {
fmt.Println("Calculating area for rectangle")
}
middleCoordinates.go
package main
import "fmt"
type MiddleCoordinates struct {
x int
y int
}
func (a *MiddleCoordinates) visitForSquare(s *Square) {
// Calculate middle point coordinates for square.
// Then assign in to the x and y instance variable.
fmt.Println("Calculating middle point coordinates for square")
}
func (a *MiddleCoordinates) visitForCircle(c *Circle) {
fmt.Println("Calculating middle point coordinates for circle")
}
func (a *MiddleCoordinates) visitForrectangle(t *Rectangle) {
fmt.Println("Calculating middle point coordinates for rectangle")
}
main.go
package main
import "fmt"
func main() {
square := &Square{side: 2}
circle := &Circle{radius: 3}
rectangle := &Rectangle{l: 2, b: 3}
areaCalculator := &AreaCalculator{}
square.accept(areaCalculator)
circle.accept(areaCalculator)
rectangle.accept(areaCalculator)
fmt.Println()
middleCoordinates := &MiddleCoordinates{}
square.accept(middleCoordinates)
circle.accept(middleCoordinates)
rectangle.accept(middleCoordinates)
}
// Calculating area for square
// Calculating area for circle
// Calculating area for rectangle
// Calculating middle point coordinates for square
// Calculating middle point coordinates for circle
// Calculating middle point coordinates for rectangle
Rust Implementation
Example: Deserialization Model
A real-world example of the Visitor pattern is serde serialization framework and its deserialization model.
- Visitor should be implemented for a deserializable type.
- Visitor is passed to a Deserializer (an “Element” in terms of the Visitor Pattern), which accepts and drives the Visitor in order to construct a desired type.
Let’s reproduce this deserializing model in our example.
visitor.rs
use crate::{TwoValuesArray, TwoValuesStruct};
/// Visitor can visit one type, do conversions, and output another type.
///
/// It's not like all visitors must return a new type, it's just an example
/// that demonstrates the technique.
pub trait Visitor {
type Value;
/// Visits a vector of integers and outputs a desired type.
fn visit_vec(&self, v: Vec<i32>) -> Self::Value;
}
/// Visitor implementation for a struct of two values.
impl Visitor for TwoValuesStruct {
type Value = TwoValuesStruct;
fn visit_vec(&self, v: Vec<i32>) -> Self::Value {
TwoValuesStruct { a: v[0], b: v[1] }
}
}
/// Visitor implementation for a struct of values array.
impl Visitor for TwoValuesArray {
type Value = TwoValuesArray;
fn visit_vec(&self, v: Vec<i32>) -> Self::Value {
let mut ab = [0i32; 2];
ab[0] = v[0];
ab[1] = v[1];
TwoValuesArray { ab }
}
}
main.rs
#![allow(unused)]
mod visitor;
use visitor::Visitor;
/// A struct of two integer values.
///
/// It's going to be an output of `Visitor` trait which is defined for the type
/// in `visitor.rs`.
#[derive(Default, Debug)]
pub struct TwoValuesStruct {
a: i32,
b: i32,
}
/// A struct of values array.
///
/// It's going to be an output of `Visitor` trait which is defined for the type
/// in `visitor.rs`.
#[derive(Default, Debug)]
pub struct TwoValuesArray {
ab: [i32; 2],
}
/// `Deserializer` trait defines methods that can parse either a string or
/// a vector, it accepts a visitor which knows how to construct a new object
/// of a desired type (in our case, `TwoValuesArray` and `TwoValuesStruct`).
trait Deserializer<V: Visitor> {
fn create(visitor: V) -> Self;
fn parse_str(&self, input: &str) -> Result<V::Value, &'static str> {
Err("parse_str is unimplemented")
}
fn parse_vec(&self, input: Vec<i32>) -> Result<V::Value, &'static str> {
Err("parse_vec is unimplemented")
}
}
struct StringDeserializer<V: Visitor> {
visitor: V,
}
impl<V: Visitor> Deserializer<V> for StringDeserializer<V> {
fn create(visitor: V) -> Self {
Self { visitor }
}
fn parse_str(&self, input: &str) -> Result<V::Value, &'static str> {
// In this case, in order to apply a visitor, a deserializer should do
// some preparation. The visitor does its stuff, but it doesn't do everything.
let input_vec = input
.split_ascii_whitespace()
.map(|x| x.parse().unwrap())
.collect();
Ok(self.visitor.visit_vec(input_vec))
}
}
struct VecDeserializer<V: Visitor> {
visitor: V,
}
impl<V: Visitor> Deserializer<V> for VecDeserializer<V> {
fn create(visitor: V) -> Self {
Self { visitor }
}
fn parse_vec(&self, input: Vec<i32>) -> Result<V::Value, &'static str> {
Ok(self.visitor.visit_vec(input))
}
}
fn main() {
let deserializer = StringDeserializer::create(TwoValuesStruct::default());
let result = deserializer.parse_str("123 456");
println!("{:?}", result);
let deserializer = VecDeserializer::create(TwoValuesStruct::default());
let result = deserializer.parse_vec(vec![123, 456]);
println!("{:?}", result);
let deserializer = VecDeserializer::create(TwoValuesArray::default());
let result = deserializer.parse_vec(vec![123, 456]);
println!("{:?}", result);
println!(
"Error: {}",
deserializer.parse_str("123 456").err().unwrap()
)
}
// Ok(TwoValuesStruct { a: 123, b: 456 })
// Ok(TwoValuesStruct { a: 123, b: 456 })
// Ok(TwoValuesArray { ab: [123, 456] })
// Error: parse_str is unimplemented