GoLang設計模式17 - 訪客模式

robin·張發表於2021-12-18

說明

訪客模式是一種行為型設計模式。通過訪客模式可以為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)允許我們分別為SquareCircleTriangle三個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)
}

同樣,circletriangle也需要實現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類圖

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

 

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

  

 

程式碼說明

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!!!

相關文章