GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Visitor」を学ぶ"
今回は、Pythonで実装した”Visitor”のサンプルアプリをGolangで実装し直してみました。
■ Visitorパターン(ビジター・パターン)
Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。
UML class and sequence diagram
UML class diagram
□ 備忘録
書籍「増補改訂版Java言語で学ぶデザインパターン入門」の引用ですが、腹落ちしました。
Visitorとは、「訪問者」という意味です。データ構造の中にたくさんの要素が格納されており、その各要素に対して何らかの「処理」をしていくとしましょう。このとき、その「処理」のコードはどこに書くべきでしょうか?普通に考えれば、データ構造を表しているクラスの中に書きますね。でも、もし、その「処理」が一種類とは限らなかったらどうでしょう。その場合、新しい処理が必要になるたびに、データ構造のクラスを修正しなければならなくなります。
Visitor
パターンでは、データ構造と処理を分離します。そして、データ構造の中をめぐり歩く主体である「訪問者」を表すクラスを用意し、そのクラスに処理をまかせます。すると、新しい処理を追加したいときには新しい「訪問者」を作ればよいことになります。そして、データ構造の方は、戸を叩いてくる「訪問者」を受け入れてあげればよいのです。
■ "Visitor"のサンプルプログラム
実際に、Visitorパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ちなみに、Qiita記事「Golangで、デザインパターン「Composite」を学ぶ」でのサンプルプログラムと挙動が同じになるので、実装を比較してみるとVisitor
パターンの理解がより深まります。
- ルートエントリの
ディレクトリ
に、サブディレクトリ
およびファイル
を追加してみる - ルートエントリの
ディレクトリ
に、ユーザエントリのディレクトリ
を追加して、さらに、サブディレクトリ
およびファイル
を追加してみる - 敢えて、
ファイル
に、ディレクトリ
を追加して、失敗することを確認する
$ go run Main.go
Making root entries
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making user entries
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/composite.py (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)
Occurring Exception...
FileTreatmentException
■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Visitor
- ディレクトリ構成
.
├── Main.go
└── visitor
├── element.go
└── visitor.go
(1) Visitor(訪問者)の役
Visitor
役は、データ構造の具体的な要素(ConcreteElement
役)ごとに、「xxxxを訪問した」というvisit(xxxx)メソッドを宣言します。visit(xxxx)はxxxxを処理するためのメソッドです。実際のコードはConcreteVisitor
役の側に書かれます。
サンプルプログラムでは、Visitor
インタフェースが、この役を努めます。
package visitor
import "fmt"
// Visitor is interface
type Visitor interface {
visit(directory Entry)
}
(2) ConcreteVisitor(具体的訪問者)の役
ConcreteVisitor
役は、Visitor
役のインタフェースを実装します。visitor(xxxx)という形のメソッドを実装し、個々のConcreteElement
役ごとの処理を記述します。
サンプルプログラムでは、ListVistor
構造体が、この役を努めます。
// ListVisitor is struct
type ListVisitor struct {
currentdir string
}
// NewListVistor func for initializing ListVisitor
func NewListVistor() *ListVisitor {
return &ListVisitor{
currentdir: "",
}
}
func (l *ListVisitor) visit(directory Entry) {
fmt.Printf("%s/%s\n", l.currentdir, directory.toString())
if _, ok := directory.(*Directory); ok {
savedir := l.currentdir
l.currentdir = fmt.Sprintf("%s/%s", l.currentdir, directory.getName())
for _, f := range directory.getDir() {
f.Accept(l)
}
l.currentdir = savedir
}
}
(3) Element(要素)の役
Element
役は、Visitor
役の訪問先を表す役です。訪問先を受け入れるaccept
メソッドを宣言します。accept
メソッドの引数にはVisitor
役が渡されます。
サンプルプログラムでは、Entry
インタフェースが、この役を努めます。
package visitor
import "fmt"
// Entry is interface
type Entry interface {
getName() string
getSize() int
Accept(v Visitor)
toString() string
getDir() []Entry
}
(4) ConcreteElement(具体的要素)の役
ConcreteElement
役は、Element
役のインタフェースを実装する役です。
サンプルプログラムでは、File
構造体とDirectory
構造体が、この役を努めます。
// File is struct
type File struct {
Entry
name string
size int
}
// NewFile func for initializing File
func NewFile(name string, size int) *File {
return &File{
name: name,
size: size,
}
}
func (f *File) getName() string {
return f.name
}
func (f *File) getSize() int {
return f.size
}
// Add func for adding file
func (f *File) Add(entry Entry) {
if err := doError(); err != nil {
fmt.Println(err)
}
}
// Accept func for accepting something
func (f *File) Accept(v Visitor) {
v.visit(f)
}
func (f *File) toString() string {
return fmt.Sprintf("%s (%d)", f.getName(), f.getSize())
}
// Directory is sturct
type Directory struct {
name string
dir []Entry
}
// NewDirectory func for initializing Directory
func NewDirectory(name string) *Directory {
return &Directory{
name: name,
}
}
func (d *Directory) getName() string {
return d.name
}
func (d *Directory) getSize() int {
size := 0
for _, f := range d.dir {
size += f.getSize()
}
return size
}
// Add func for adding directory
func (d *Directory) Add(entry Entry) {
d.dir = append(d.dir, entry)
}
// Accept func for accepting something
func (d *Directory) Accept(v Visitor) {
v.visit(d)
}
func (d *Directory) toString() string {
return fmt.Sprintf("%s (%d)", d.getName(), d.getSize())
}
func (d *Directory) getDir() []Entry {
return d.dir
}
(5) ObjectStructure(オブジェクトの構造)の役
ObjectStructure
役は、Element
役の集合を扱う役です。ConcreteVisitor
役が個々のElement
役を扱えるようなメソッドを備えています。
サンプルプログラムでは、Directory
構造体がこの役を努めます。(一人二役です)
(6) Client(依頼人)の役
サンプルプログラムでは、startMain
関数が、この役を努めます。
package main
import (
"fmt"
"./visitor"
)
func startMain() {
fmt.Println("Making root entries")
rootdir := visitor.NewDirectory("root")
bindir := visitor.NewDirectory("bin")
tmpdir := visitor.NewDirectory("tmp")
usrdir := visitor.NewDirectory("usr")
rootdir.Add(bindir)
rootdir.Add(tmpdir)
rootdir.Add(usrdir)
bindir.Add(visitor.NewFile("vi", 10000))
bindir.Add(visitor.NewFile("latex", 20000))
rootdir.Accept(visitor.NewListVistor())
fmt.Println("")
fmt.Println("Making user entries")
yuki := visitor.NewDirectory("yuki")
hanako := visitor.NewDirectory("hanako")
tomura := visitor.NewDirectory("tomura")
usrdir.Add(yuki)
usrdir.Add(hanako)
usrdir.Add(tomura)
yuki.Add(visitor.NewFile("diary.html", 100))
yuki.Add(visitor.NewFile("composite.py", 200))
hanako.Add(visitor.NewFile("memo.tex", 300))
tomura.Add(visitor.NewFile("game.doc", 400))
tomura.Add(visitor.NewFile("junk.mail", 500))
rootdir.Accept(visitor.NewListVistor())
fmt.Println("")
fmt.Println("Occurring Exception...")
tmpfile := visitor.NewFile("tmp.txt", 100)
bindir = visitor.NewDirectory("bin")
tmpfile.Add(bindir)
}
func main() {
startMain()
}
(7) その他
エラー時の振る舞いを追加します
func doError() error {
msg := "FileTreatmentException"
return fmt.Errorf("%s", msg)
}