初めに
最近、オブジェクト指向プログラミングのデザインパターンの一つである「Visitor Pattern」を学びました。
今回はそのVisitor Patternについて説明し、さらにGo言語で簡単なVisitor Patternのサンプルを実装してみます。
Visitor Pattern とは
Visitor Patternは「データ構造」と「処理」を分離することを目的としたデザインパターンです。そのように分離することで、データ構造を変更することなく後から処理をどんどん追加していくことができるというメリットがあります。
具体的に説明すると、Visitor Patternには
- Acceptor
- Visitor
という二つの構成要素があります。
Acceptorはデータ構造を定義するオブジェクトで、acceptメソッドを持ちます。acceptメソッドでは引数にVisitorオブジェクトを取り、Visitorオブジェクトのvisitメソッドを呼び出してその引数に自分自身(Acceptorオブジェクト)を渡します。具体的な処理はVisitorが担うので、acceptメソッド内にロジックは含まれません。
Visitorは処理を定義するオブジェクトで、visitメソッドを持ちます。visitメソッドでは引数にAcceptorオブジェクトを取り、そのAcceptorオブジェクトが公開しているリソースを利用して何らかの処理を行います。
大事なのは、処理の記述はVisitorのvisitメソッド内のみに定義され、Acceptorには処理が記述されないという点です。この設計により、何らかの新しい振る舞いが必要になった場合は新規のVisitorを定義してそのvisitメソッド内に各Acceptorに対する処理を記述するだけでよく、Acceptorには変更を加える必要がないという恩恵が得られます。
上述した通り、Visitor Patternはデータ構造を定義するAcceptorと、それに依存して処理を定義するVisitor、という美しい依存関係の設計になっています。また、Acceptorのacceptメソッドの引数にはVisitorオブジェクトを取るので一見AcceptorがVisitorの実装に依存しているように思えてしまいますが、実際にはAcceptorが依存するのはvisitメソッドを持つVisitorインターフェースになるので、AcceptorがVisitorの実装には依存しない設計を保てています。
GoでのVisitor Patternのサンプル
さて、理論だけではなかなか理解が難しいので、実際のサンプルを見てみましょう。
今回はGoで簡単なVisitor Patternのサンプルを実装してみました。リポジトリはこちらです。
オブジェクト間の依存関係は以下のようになっています。
※Goはオブジェクト指向言語ではなくクラスや継承といった機能がないので、実際にはstruct型のtypeを定義してオブジェクト指向っぽいことをしています。
また、ディレクトリ構造は以下のようになっています。
├── acceptor
│ ├── _interface
│ │ ├── acceptor.go
│ │ └── visitor.go
│ └── impl
│ ├── acceptorA.go
│ └── acceptorB.go
├── go.mod
├── main.go
└── visitor
└── impl
├── visitorA.go
└── visitorB.go
※Goではパッケージが相互参照するとimports cycle not allowed
という循環参照エラーが発生してしまうので、visitorパッケージからacceptorパッケージへの一方的な参照となるようにVisitorインターフェースの定義をaccetptorパッケージ側に置いています。もっと上手い方法があればぜひ教えて欲しいです...
Visitor、Acceptorのインターフェースは以下のような定義になっています。
package _interface
type Visitor interface {
Visit(acceptor Acceptor)
}
package _interface
type Acceptor interface {
Accept(visitor Visitor)
}
それぞれvisitメソッドとacceptメソッドのシグネチャだけが定義されています。
また、具体的なAcceptorとVisitorの例を以下に示します。
package impl
import (
"example.com/visitor-pattern/acceptor/_interface"
)
func NewAcceptorA() _interface.Acceptor {
return AcceptorA{name: "acceptorA"}
}
type AcceptorA struct {
name string
}
func (impl AcceptorA) Accept(visitor _interface.Visitor) {
visitor.Visit(impl)
}
func (impl AcceptorA) GetName() string {
return impl.name
}
package impl
import (
"fmt"
acceptorInterface "example.com/visitor-pattern/acceptor/_interface"
acceptorImpl "example.com/visitor-pattern/acceptor/impl"
)
func NewVisitorA() acceptorInterface.Visitor {
return implA{name: "visitorA"}
}
type implA struct {
name string
}
func (impl implA) Visit(acceptor acceptorInterface.Acceptor) {
switch acceptor.(type) {
case acceptorImpl.AcceptorA:
fmt.Println(impl.name + "が" + acceptor.(acceptorImpl.AcceptorA).GetName() + "を訪問しました。")
case acceptorImpl.AcceptorB:
fmt.Println(impl.name + "が" + acceptor.(acceptorImpl.AcceptorB).GetName() + "を訪問しました。")
}
}
Acceptorのacceptメソッドでは、上述した通りに引数のVisitorメソッドのvisitメソッドを呼び出し、その引数に自分自身を渡すだけになっています。
そしてVisitorのvisitメソッドでは、引数のAcceptorの型に応じて処理を分岐し、擬似的なoverloadを行なっています。
これらのオブジェクトを用いて、
package main
import (
acceptor "example.com/visitor-pattern/acceptor/impl"
visitor "example.com/visitor-pattern/visitor/impl"
)
func main() {
visitorA := visitor.NewVisitorA()
visitorB := visitor.NewVisitorB()
acceptorA := acceptor.NewAcceptorA()
acceptorB := acceptor.NewAcceptorB()
acceptorA.Accept(visitorA)
acceptorA.Accept(visitorB)
acceptorB.Accept(visitorA)
acceptorB.Accept(visitorB)
}
上記のようなコードを実行すると、以下の結果が得られます。
❯ go run ./main.go
visitorAがacceptorAを訪問しました。
visitorBがacceptorAを訪問しました。
visitorAがacceptorBを訪問しました。
visitorBがacceptorBを訪問しました。
もし新しい振る舞いを追加したければ、以下のように新規のVisitorを作成します。
package impl
import (
"fmt"
acceptorInterface "example.com/visitor-pattern/acceptor/_interface"
acceptorImpl "example.com/visitor-pattern/acceptor/impl"
)
func NewVisitorC() acceptorInterface.Visitor {
return implC{name: "visitorC", present: "花束"}
}
type implC struct {
name string
present string
}
func (impl implC) Visit(acceptor acceptorInterface.Acceptor) {
switch acceptor.(type) {
case acceptorImpl.AcceptorA:
fmt.Println(impl.name + "が" + impl.present + "を渡しに" + acceptor.(acceptorImpl.AcceptorA).GetName() + "を訪問しました。")
case acceptorImpl.AcceptorB:
fmt.Println(impl.name + "が" + impl.present + "を渡しに" + acceptor.(acceptorImpl.AcceptorB).GetName() + "を訪問しました。")
}
}
package main
import (
acceptor "example.com/visitor-pattern/acceptor/impl"
visitor "example.com/visitor-pattern/visitor/impl"
)
func main() {
visitorA := visitor.NewVisitorA()
visitorB := visitor.NewVisitorB()
+ visitorC := visitor.NewVisitorC()
acceptorA := acceptor.NewAcceptorA()
acceptorB := acceptor.NewAcceptorB()
acceptorA.Accept(visitorA)
acceptorA.Accept(visitorB)
+ acceptorA.Accept(visitorC)
acceptorB.Accept(visitorA)
acceptorB.Accept(visitorB)
+ acceptorB.Accept(visitorC)
}
visitorCに新たにpresent
フィールドが加わっているように、各Visitorが訪問先のAcceptorで何をするかはそれぞれの実装によって変えることができます。
このコードを実行すると、以下のような結果が得られます。
❯ go run ./main.go
visitorAがacceptorAを訪問しました。
visitorBがacceptorAを訪問しました。
visitorCが花束を渡しにacceptorAを訪問しました。
visitorAがacceptorBを訪問しました。
visitorBがacceptorBを訪問しました。
visitorCが花束を渡しにacceptorBを訪問しました。
最後に
今回は、オブジェクト指向のデザインパターンの一つである「Visitor Pattern」をGoで実装してみました。
「データ構造」と「処理」を分離する、という設計が適しているユースケースがあれば、ぜひ使ってみようと思います。
最後まで読んでいたただき、ありがとうございました。