冬のセール!
Visitor

Visitor を Go で

Visitor 振る舞いに関するデザインパターンの一つで 既存コードを変更することなく 既存のクラス階層に新しい振る舞いの追加を可能とします

Visitor の代わりに単純にメソッドの多重定義 overload を使うことができない理由については 別の記事 ビジターと二重ディスパッチ を参照

概念的な例

Visitor パターンを使うと ある構造体への新しい振る舞いの追加を 構造体そのものを変更することなく行えます 以下のような図形を表現する構造体を含むライブラリーの保守をしているとしましょう

  • Square 正方形
  • Circle
  • Triangle 三角形

上記の図形構造体は それぞれ共通の図形インターフェースを実装します

会社の人たちが あなたの素晴らしいライブラリーを使い始めるとすぐに 新機能リクエストが殺到しました 一番簡単なのを見てみましょう あるチームからは shape 構造体に get­Area 面積を取得 機能を追加して欲しいというリクエストが来ました

この問題を解決するには いくつかの選択肢があります

最初に思い浮かぶ選択肢は shape インターフェースに直接 get­Area メソッドを追加し shape を実装した構造体それぞれで実装することです これは まったくもっともで すぐに実施すべきなように思えますが タダというわけではありません ライブラリーの保守担当者としては 誰かが新機能をリクエストするたびに 大切なコードを壊してしまうリスクを負いたくありません それでも 他のチームにライブラリーの拡張ができるようにしてあげたいと思っています

二つ目の選択肢は 新機能をリクエストしているチームが自分たちでそれを実装できるようにすることです しかし 新規の振る舞いは非公開のコードに依存しているかもしれないので これが常に可能とは限りません

三つ目の選択肢は Visitor パターンを使ってこの問題を解くことです まず このようなビジター・インターフェースを定義します

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

visit­For­Square­(square) visit­For­Circle­(circle) visit­For­Triangle­(triangle) といった関数は それぞれ 正方形 三角形に機能を追加します

何でビジター・インターフェースに visit­(shape) というメソッドを一つだけ追加しないんだと疑問に思いますか 理由は Go 言語がメソッドの多重定義をサポートしていないからです パラメーターだけ異なり名前が同じメソッドは許可されていません

二つ目の重要な事項は shape インターフェースに accept メソッドを追加することです

func accept(v visitor)

shape 構造体はすべて このメソッドを定義する必要があります

func (obj *square) accept(v visitor){
    v.visitForSquare(obj)
}

ちょっと待って さっき既存の構造体は変更したくないと言わなかったですか 残念なことに Visitor パターンを利用する時は shape の構造体を変更する必要があります しかし この変更は一度だけです

get­Num­Sides とか get­Middle­Coordinates のような振る舞いを追加する場合でも 同じ accept­(v visitor) 関数が使えるので shape 構造体への更なる変更は不要です

つまり shape 構造体は一度だけ変更する必要があり 将来のすべての機能追加リクエストは 同一の accept 関数で対処できます チームが get­Area 機能の追加をリクエストした場合 単にビジター・インターフェースの具象実装を定義し そこに面積計算のロジックを書くだけですみます

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)
}

output.txt: 実行結果

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

他言語での Visitor

Visitor を C# で Visitor を C++ で Visitor を Java で Visitor を PHP で Visitor を Python で Visitor を Ruby で Visitor を Rust で Visitor を Swift で Visitor を TypeScript で