Goでデザインパターン (Visitorパターン)

Goデザインパターン

Visitorパターンとは

振る舞いに関するデザインパターンの一つ。処理(アルゴリズム)をデータ構造(オブジェクト)から分離する。

長所

  • 様々なオブジェクトと動作可能な新規の振る舞いを既存クラスに変更を加えることなく導入することができる。(開放閉鎖の原則)
  • 種々のオブジェクトに対する同じ振る舞いを同一クラスにまとめることができる。(単一責任の原則)
  • ツリー構造の探索などの時、Visitorオブジェクトは様々なオブジェクトと動作しながら内部状態を変化することができる。

短所

  • 新たな要素を追加するたびに、すべてのVisitorクラスにその要素用の処理を追加する必要がある。
  • 各オブジェクトの非公開フィールド・メソッドを利用する必要がある場合は、Visitorパターンでは実装できない場合がある。

利用場面

  • オブジェクトツリー等の複雑なオブジェクト構造のすべての要素に対してある処理を実行する必要がある場合。
    (同一処理を異なるクラスのオブジェクトに対して実行できる)
  • 補助的な振る舞いのビジネスロジックをまとめたい場合

クラス図

  • Visitor
    具象Elementのオブジェクトを受け取るVisitメソッドを宣言する。メソッドは具象Element毎に必要。メソッドのオーバーロードが実装されている言語の場合は同一名のメソッドで宣言してもよい。
  • Concrete Visitors
    Visitorで宣言されたインターフェースを実装する。個々のConcrete Element毎の処理を実装する。
    処理の過程でConcrete Visitorの内部状態が変化する場合もある。
  • Element
    Visitorを受け入れるためのAcceptメソッドを宣言する。このメソッドの引数はVisitorインターフェース。
  • Concrete Elements
    Elementで宣言されたインターフェースを実装する。AcceptメソッドではVisitorで宣言されているインターフェースの内、適切なメソッドを呼び出す。
  • Client
    扱っているオブジェクトがどのConcrete Elementのオブジェクトか認識することなく処理を実行できる。

実装例

Visitor

package main

type Visitor interface {
	// 各要素ごとの処理を宣言する。
	// メソッドのオーバーロードが許されている言語の場合は、visit(s Square), vist(c Circle)...のように宣言できる。
	VisitForSquare(*Square)
	VisitForCircle(*Circle)
	VisitForRectangle(*Rectangle)
}

Concrete Visitors

package main

import (
	"math"
	"fmt"
)

// 具象Visitorは要素別のメソッドを定義する。
type AreaCalculator struct {}

func NewAreaCalculator() *AreaCalculator {
	return &AreaCalculator{}
}

func (a *AreaCalculator) VisitForSquare(s *Square) {
	sa := math.Pow(float64(s.GetSide()) , 2)
	fmt.Printf("square area: %.2f\n", sa)
}

func (a *AreaCalculator) VisitForCircle(c *Circle) {
	ca :=  math.Pow(float64(c.GetRadius()) , 2) * math.Pi
	fmt.Printf("circle area: %.2f\n", ca)
}

func (a *AreaCalculator) VisitForRectangle(r *Rectangle) {
	ra := r.GetShortSide() * r.GetLongSide()
	fmt.Printf("rectangle area: %.2f\n", ra)
}
package main

import (
	"math"
	"fmt"
)

type PerimeterCalculator struct {}

func NewPerimeterCalculator() *PerimeterCalculator {
	return &PerimeterCalculator{}
}

func (p *PerimeterCalculator) VisitForSquare(s *Square) {
	sp := s.GetSide() * 4
	fmt.Printf("square perimeter: %.2f\n", sp)
}

func (p *PerimeterCalculator) VisitForCircle(c *Circle) {
	cv := float64(c.GetRadius()) * 2 * math.Pi
	fmt.Printf("circle perimeter: %.2f\n", cv)
}

func (p *PerimeterCalculator) VisitForRectangle(r *Rectangle) {
	rv := (r.GetShortSide() + r.GetLongSide()) * 2
	fmt.Printf("rectangle perimeter: %.2f\n", rv)
}
package main

import (
	"fmt"
)

type JpegExporter struct {}

func NewJpegExporter() *JpegExporter {
	return &JpegExporter{}
}

func (p *JpegExporter) VisitForSquare(s *Square) {
	fmt.Println("export square fugure to jpeg")
}

func (p *JpegExporter) VisitForCircle(c *Circle) {
	fmt.Println("export circle fugure to jpeg")
}

func (p *JpegExporter) VisitForRectangle(r *Rectangle) {
	fmt.Println("export rectangle fugure to jpeg")
}

Element

package main

// ElementインターフェースはVisitorインターフェースを引数としたacceptメソッドを宣言する。
type Shape interface {
	Accept(v Visitor)
}

Concrete Elements

package main

type Square struct {
	side float64
}

func NewSquare(side float64) *Square {
	return &Square{side: side}
}

// acceptメソッド内でvisitorの各要素ごとに実装されたメソッドを呼び出す。
// メソッドのオーバーーロードが許される言語の場合はvisit(self)のようにできる。
func (s *Square) Accept(v Visitor) {
	// Goではメソッドのオーバーロードができないため、ここで要素ごとのメソッドを呼び出す。
	v.VisitForSquare(s)
}

func (s *Square) GetSide() float64 {
	return s.side
}
package main

type Circle struct {
	radius int
}

func NewCircle(radius int) *Circle {
	return &Circle{radius: radius}
}

func (c *Circle) Accept(v Visitor) {
	v.VisitForCircle(c)
}

func (c *Circle) GetRadius() int {
	return c.radius
}
package main

type Rectangle struct {
	square Square
	ratio float64
}

func NewRectangle(shortSide float64, ratio float64) *Rectangle {
	return &Rectangle{
		square: *NewSquare(shortSide),
		ratio: ratio,
	}
}

func (r *Rectangle) Accept(v Visitor) {
	v.VisitForRectangle(r)
}

func (r *Rectangle) GetShortSide() float64 {
	return r.square.side
}

func (r *Rectangle) GetLongSide() float64 {
	return r.square.side * r.ratio
}

動作確認

package main

import (
	"fmt"
)

func main() {
	shapes := []Shape{
		NewSquare(5.6),
		NewCircle(1),
		NewRectangle(3, 2.5),
	}

	// クライアント側は具体的な要素を把握することなくVisitorの操作を実行可能。
	for _, shape := range shapes {
		areaCalculator := NewAreaCalculator()
		perimeterCalculator := NewPerimeterCalculator()
		exporter := NewJpegExporter()
		shape.Accept(areaCalculator)
		shape.Accept(perimeterCalculator)
		shape.Accept(exporter)
		fmt.Println()
	}

	// Goで直接Visitorのメソッドを呼び出す場合は、クライアント側でオブジェクトが何の構造体かを知る必要がある。
	// メソッドのオーバーロードが許されている場合でも、親要素と子要素の区別ができないため2重ディスパッチが必要になる。
	// areaCalculator.VisitForSquare(square)
}
>> go run .
square area: 31.36
square perimeter: 22.40
export square fugure to jpeg

circle area: 3.14
circle perimeter: 6.28
export circle fugure to jpeg

rectangle area: 22.50
rectangle perimeter: 21.00
export rectangle fugure to jpeg

コメント

タイトルとURLをコピーしました