行为型 - 7. 访问者模式

88 阅读4分钟

访问者模式难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议不要使用访问者模式。

1. 访问者模式的原理

访问者模式的英文翻译是 Visitor Design Pattern。允许一个或者多个操作应用到一组对象上,解耦操作和对象本身

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

一般来说,访问者模式针对的是一组类型不同的对象,尽管这组对象的类型是不同的,但是,它们继承相同的父类或者实现相同的接口。在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作,但为了避免不断添加功能导致类不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类中。

2. 访问者模式的难点

对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。

正是因为代码实现难理解,所以,在项目中应用这种模式,会导致代码的可读性比较差。如果你的同事不了解这种设计模式,可能就会读不懂、维护不了你写的代码。所以,除非不得已,不要使用这种模式。

如果不使用访问者模式,可以使用工厂模式,在 map 中保存 type 和具体子类实例的映射,在使用的时候,根据 type 的不同调用不同子类的方法(动态绑定)。

如果操作功能并不是非常多,只有几个而已,那更推荐使用工厂模式的实现方式,毕竟代码更加清晰、易懂。相反,如果操作功能非常多,比如有十几个,那更推荐使用访问者模式,因为访问者模式需要定义的类要比工厂模式的实现方式少很多,类太多也会影响到代码的可维护性。

3. 为什么支持双分派的语言不需要访问者模式

  • Single Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。
  • Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。

目前主流的语言都只支持 Single Dispatch,不支持 Double Dispathch。

4. 访问者模式的代码实现


type shape interface {
   getType() string
   accept(visitor)
}

type square struct {
   side int
}

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

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

type circle struct {
   radius int
}

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

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

type rectangle struct {
   length int
   width  int
}

func (t *rectangle) accept(v visitor) {
   v.visitFoRectangle(t)
}

func (t *rectangle) getType() string {
   return "rectangle"
}


type visitor interface {
   // 定义多个抽象方法,是因为 golang 不支持方法重载,简单点说,就是不支持方法同名
   visitForSquare(*square)
   visitForCircle(*circle)
   visitFoRectangle(*rectangle)
}

type areaCalculator struct {
}

func (a *areaCalculator) visitForSquare(s *square) {
   //Calculate area for square. After calculating the area assign in to the area instance variable
   area := s.side * s.side
   fmt.Printf("Calculating area for square, side: %d, area: %d\n", s.side, area)
}

func (a *areaCalculator) visitForCircle(s *circle) {
   //Calculate are for circle. After calculating the area assign in to the area instance variable
   radius := float64(s.radius)
   area := math.Pi * radius * radius
   fmt.Printf("Calculating area for circle radisu: %.3f, area: %.3f\n", radius, area)
}

func (a *areaCalculator) visitFoRectangle(s *rectangle) {
   //Calculate are for rectangle. After calculating the area assign in to the area instance variable
   area := s.length * s.width
   fmt.Printf("Calculating area for rectangle, length: %d, width: %d, area: %d\n", s.length, s.width, area)
}

type middleCoordinates struct {
}

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) visitFoRectangle(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")
}

// 客户端代码实现
func TestVisitor(t *testing.T) {
   square := &square{side: 2}
   circle := &circle{radius: 2}
   rectangle := &rectangle{
      length: 2,
      width:  3,
   }

   var visitor visitor

   visitor = &areaCalculator{}
   square.accept(visitor)
   circle.accept(visitor)
   rectangle.accept(visitor)

   visitor = &middleCoordinates{}
   square.accept(visitor)
   circle.accept(visitor)
   rectangle.accept(visitor)
}