天天看點

GoLang設計模式17 - 訪客模式

說明

訪客模式是一種行為型設計模式。通過訪客模式可以為struct添加方法而不需要對其做任何調整。

來看一個例子,假如我們需要維護一個對如下形狀執行操作的庫:

  1. 方形(Square)
  2. 圓形(Circle)
  3. 長方形(Rectangle)

以上圖形的struct都繼承自一個共同的

shape

接口。公司内有多個團隊都在使用這個庫。假設現在有一個團隊想要為這些圖形struct添加一個擷取面積的方法(

getArea()

)。有如下幾種方法可以解決這種問題。

方法一

第一種方案就是直接在

shape

接口中添加

getArea()

方法。這樣實作

shape

接口的每個struct都需要實作

getArea()

方法。這個方案看起來可行,但是卻有一些問題:

  1. 作為一個公用庫的維護者,有時候不想為了添加一個額外的行為就調整已經做過嚴格測試的代碼
  2. 使用這個庫的團隊可能會提出添加更多行為的請求,比如

    getNumSides()

    getMiddleCoordinates()

    。面對這種情況,我們通常都不想持續修改這個庫,而是希望這些團隊繼承我們這個庫,自己實作自己的需求。

方法二

第二個方案就是由提出需求的團隊自己實作相關的行為邏輯。要擷取

shape

的面積就可以根據struct的類型作如下的實作:

if shape.type == square {
   //Calculate area for squre
} elseif shape.type == circle {
    //Calculate area of triangle 
} elseif shape.type == "triangle" {
    //Calculate area of triangle
} else {
   //Raise error
} 
      

以上的代碼仍然是有問題的:這種方案不能充分利用接口的特性,反而還需要添加額外的類型來檢查代碼,造成整體結構的脆弱。此外,在運作時擷取對象的類型可能會存在一些性能問題,在一些語言中甚至還不能擷取對象的類型。

方法三

第三種方案就是使用訪客模式來解決這個問題。我們可以定義一個如下的訪客接口:

type visitor interface {

   visitForSquare(square)

   visitForCircle(circle)

   visitForTriangle(triangle)
}
      

接口中的三個函數

visitforSquare(square)

visitForTriangle(triangle)

visitForCircle(circle)

允許我們分别為

Square

Circle

Triangle

三個struct分别添加函數。

現在可以開始考慮一個問題了:為什麼我們不在

visitor

接口中添加一個

visit(shape)

方法,而是為每種形狀單獨寫了一個visit方法?原因很簡單:Go語言不支援。

接下來在

shape

accept

方法:

func accept(v visitor)
      

每一個實作

shape

的struct都需要定義這個方法。額,等等,我們剛才好像提到過不想修改現有的

shape

 struct。但是要使用訪客模式就不得不修改相關的

shape

 struct,不過這些修改隻需要做一次。假如我們還希望添加注入

getNumSides()

(擷取邊數)、

getMiddleCoordinates()

(擷取中心坐标),此時就不需要再對相關的struct做任何調整了,可以直接使用前面定義的

accept(v visitor)

方法了。

基本上就是這樣了,隻需修改實作

shape

接口的struct一次,之後想再添加多少個額外的行為都可以使用同一個

accept()

方法。接下來看下具體是怎麼做的。

讓struct 

square

實作一個

accept()

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

同樣,

circle

triangle

也需要實作

accept()

方法。

現在想要添加

getArea()

方法的團隊就可以實作

visitor

接口并在相關的方法中自行添加計算面積的邏輯:

如areaCalculator.go:

type areaCalculator struct{
    area int
}

func (a *areaCalculator) visitForSquare(s *square){
    //Calculate are for square
}
func (a *areaCalculator) visitForCircle(s *square){
    //Calculate are for circle
}
func (a *areaCalculator) visitForTriangle(s *square){
    //Calculate are for triangle
}
      

比如要計算正方形的面積,我們先建立一個

square

執行個體,然後進行如下簡單的調用就可以了:

sq := &square{}
ac := &areaCalculator{}
sq.accept(ac)
      

同理,另一個想要擷取形狀中心坐标的團隊也可以像前面那樣自己實作

visitor

接口并添加相關的方法:

middleCoordinates.go:

type middleCoordinates struct {
    x int
    y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}

func (a *middleCoordinates) visitForCircle(c *circle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}

func (a *middleCoordinates) visitForTriangle(t *triangle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}      

UML類圖

通過前面的說明,我們可以總結出訪客模式的類圖:

GoLang設計模式17 - 訪客模式

如下是前面的例子的類圖:

  

GoLang設計模式17 - 訪客模式

代碼說明

shape.go:

type shape interface {
	
	getType() string

	accept(visitor)
}
      

square.go:

type square struct {
	side int
}

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

func (s *square) getType() string {
	return "Square"
}
      

circle.go:

type circle struct {
	radius int
}

func (c *circle) accept(v visitor) {
	v.visitForCircle(c)
}

func (c *circle) getType() string {
	return "Circle"
}
      

rectangle.go:

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:

type visitor interface {
	
	visitForSquare(*square)

	visitForCircle(*circle)

	visitForRectangle(*rectangle)
}
      

areaCalculator.go:

type areaCalculator struct {
	area int
}

func (a *areaCalculator) visitForSquare(s *square) {
	//Calculate area for square. After calculating the area assign in to the area instance variable
	fmt.Println("Calculating area for square")
}

func (a *areaCalculator) visitForCircle(s *circle) {
	//Calculate are for circle. After calculating the area assign in to the area instance variable
	fmt.Println("Calculating area for circle")
}

func (a *areaCalculator) visitForRectangle(s *rectangle) {
	//Calculate are for rectangle. After calculating the area assign in to the area instance variable
	fmt.Println("Calculating area for rectangle")
}
      

middleCoordinates.go:

type middleCoordinates struct {
	x int
	y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
	//Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
	fmt.Println("Calculating middle point coordinates for square")
}

func (a *middleCoordinates) visitForCircle(c *circle) {
	//Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
	fmt.Println("Calculating middle point coordinates for circle")
}

func (a *middleCoordinates) visitForRectangle(t *rectangle) {
	//Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
	fmt.Println("Calculating middle point coordinates for rectangle")
}
      

main.go:

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

輸出内容為:

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
      

代碼已上傳至GitHub: zhyea / go-patterns / visitor-pattern

END!!!

僅是學習筆記,難免出錯,望不吝指點