访问者模式:将数据结构对象与数据操作分离的设计模式,可以在不改变数据结构对象类结构的前提下定义作用于这些对象的新的操作。
访问模式结构:
- 访客接口 (Visitor) 声明了一系列以表示对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。
- 具体访客 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
- 元素 (Element) 接口,声明了一个方法来 "接收" 访问者。 该方法必须有一个参数被声明为访问者接口类型。
- 具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。 请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访客对象中的合适方法。
有点像迭代器模式的结构,可以把迭代器看成访问者模式的特例,迭代器是吧遍历操作与具体数据结构解耦,而访问者模式把任何操作与一类对象解耦。
访问者模式通过将算法与对象结构分离来工作,这里说的算法指的是对对象的操作。为此,需要定义了一个表示算法的接口--Visitor。该接口将为对象结构中的每个类(一般称为元素类)提供一个方法。每个方法都将元素类的一个实例作为参数。表示对象结构的所有元素类也会实现一个Element接口,该接口定义了接受访问者的方法Accpet。此方法将访问者接口的实现作为参数。当Accpet方法被调用时,访问者实例对应的方法就会被调用,通过访问者完成对元素类实例的操作。
访问者模式允许你在结构体中添加行为, 而又不会对结构体造成实际变更。 假设代码库中包含不同的形状结构体, 如:
- 方形
- 圆形
- 三角形
上述每个形状结构体都实现了通用形状接口。
首先定义一个如下访问者接口:
go
type visitor interface {
visitForSquare(square)
visitForCircle(circle)
visitForTriangle(triangle)
}
可以使用 visitForSquare(square)
、 visitForCircle(circle)
以及 visitForTriangle(triangle)
函数来为方形、 圆形以及三角形添加相应的功能。
将 accept
接受方法添加至形状接口中。如果添加任何其他行为, 比如 getNumSides
获取边数和 getMiddleCoordinates
获取中点坐标 , 可使用相同的 accept(v visitor)
函数, 而无需对形状结构体进行进一步的修改。
go
func accept(v visitor)
定义对象接口
go
type Shape interface {
getType() string
accept(Visitor)
}
实现接口
go
type Square struct {
side int
}
func (s *Square) accept(v Visitor) {
v.visitForSquare(s)
}
func (s *Square) getType() string {
return "Square"
}
go
type Circle struct {
radius int
}
func (c *Circle) accept(v Visitor) {
v.visitForCircle(c)
}
func (c *Circle) getType() string {
return "Circle"
}
go
type Rectangle struct {
l int
b int
}
func (t *Rectangle) accept(v Visitor) {
v.visitForrectangle(t)
}
func (t *Rectangle) getType() string {
return "rectangle"
}
实现不同的访问方法
go
type AreaCalculator struct {
area int
}
func (a *AreaCalculator) visitForSquare(s *Square) {
// Calculate area for square.
// Then assign in to the area instance variable.
fmt.Println("Calculating area for square")
}
func (a *AreaCalculator) visitForCircle(s *Circle) {
fmt.Println("Calculating area for circle")
}
func (a *AreaCalculator) visitForrectangle(s *Rectangle) {
fmt.Println("Calculating area for rectangle")
}
go
package main
import "fmt"
type MiddleCoordinates struct {
x int
y int
}
func (a *MiddleCoordinates) visitForSquare(s *Square) {
// Calculate middle point coordinates for square.
// Then assign in to the x and y instance variable.
fmt.Println("Calculating middle point coordinates for square")
}
func (a *MiddleCoordinates) visitForCircle(c *Circle) {
fmt.Println("Calculating middle point coordinates for circle")
}
func (a *MiddleCoordinates) visitForrectangle(t *Rectangle) {
fmt.Println("Calculating middle point coordinates for rectangle")
}
执行代码
go
package main
import "fmt"
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)
}
执行结果
go
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
访客模式有如下优点
- 解耦了数据结构与数据操作,使得操作集合可以独立变化。
- 可以通过扩展访问者角色,实现对数据集的不同操作,程序扩展性更好。
- 元素具体类型并非单一,访问者均可操作。
- 各角色职责分离,符合单一职责原则。
与此同时它也有以下缺点
- 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则。
- 具体元素变更困难:具体元素增加属性、删除属性等操作, 会导致对应的访问者类需要进行相应的修改,尤其当有大量访客类时,修改范围太大。
- 违背依赖倒置原则:为了达到"区别对待",访问者角色依赖的是具体元素类型,而不是抽象接口。
访问者模式与其他设计模式:
- 将访问者模式视为 命令模式 的加强版本, 其对象可对不同类的多种对象执行操作。
- 使用访问者对整个 组合模式树 执行操作。
- 可以同时使用访问者和 迭代器模式 来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。