LoginSignup
1
2

More than 3 years have passed since last update.

Golangで、デザインパターン「Visitor」を学ぶ

Posted at

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Visitor」を学ぶ"

今回は、Pythonで実装した”Visitor”のサンプルアプリをGolangで実装し直してみました。

■ Visitorパターン(ビジター・パターン)

Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。

UML class and sequence diagram

W3sDesign_Visitor_Design_Pattern_UML.jpg

UML class diagram

Visitor_design_pattern.svg.png

□ 備忘録

書籍「増補改訂版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インタフェースが、この役を努めます。

visitor/visitor.go
package visitor

import "fmt"

// Visitor is interface
type Visitor interface {
    visit(directory Entry)
}

(2) ConcreteVisitor(具体的訪問者)の役

ConcreteVisitor役は、Visitor役のインタフェースを実装します。visitor(xxxx)という形のメソッドを実装し、個々のConcreteElement役ごとの処理を記述します。
サンプルプログラムでは、ListVistor構造体が、この役を努めます。

visitor/visitor.go
// 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インタフェースが、この役を努めます。

visitor/element.go
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構造体が、この役を努めます。

visitor/element.go
// 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())
}
visitor/element.go
// 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関数が、この役を努めます。

Main.go
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) その他

エラー時の振る舞いを追加します

visitor/element.go
func doError() error {
    msg := "FileTreatmentException"
    return fmt.Errorf("%s", msg)
}

■ 参考URL

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2