设计模式-访问者模式
最近学习AST(abstract static tree抽象语法树)和阅读revive-cc源码时发现,在对AST进行遍历&对每个node进行操作时,使用了访问者模式。每个节点都是不同类的对象,在对这些属于不同类的一组对象进行同一操作时,访问者模式会使相关实现变得更加优雅。
下面对访问者模式进行介绍,然后实现一个小demo,最后看看revive和revive-cc这两个针对golang的静态分析库是如何使用访问者模式的。
1 访问者模式
1.1 概念介绍
访问者模式是一种行为设计模式,它能将算法与其所作用的对象隔离开来。

乍一看上去很难懂~

某个小区里有若干个住户,物业需要进住户家里查水表、查电表、查煤气表。。。但是住户家里可不是谁都能进的,怎么验证身份呢。
一种方法是让业主来验证,先带水工挨个给业主认识一下,再带电工挨个给业主认识一下,过了几天需要查煤气表了,再带煤气工给挨个业主认识一下。实在是太麻烦了
其实有一种更简单的办法,怎么做呢?业主家门上有密码锁,物业把密码表给各个值得信赖的工人看一下,他们自己进去就行了。
在这里:
访问者接口(Visitor interface
)声明了访问方法(visit
),即拿到每个住户房门的密码;
访问者实体(ConcreteVistors
)就是工人,他们拿到了密码表就相当于实现了访问方法;
元素接口(Element interface
)是住户密码锁的抽象,接受拥有访问能力的人来访问(实现了访问者接口的工人);
各个用户的密码锁是元素实体(ElementA、ElementB
),他们有各自的特征,即密码不一致。拥有访问能力的工人可通过该实体的visit方法进行访问。
1.2 应用场景
- 如果你需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。
访问者模式通过在访问者对象中为多个目标类提供相同操作的变体, 让你能在属于不同类的一组对象上执行同一操作。 - 可使用访问者模式来清理辅助行为的业务逻辑。
该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。 - 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。
你可将该行为抽取到单独的访问者类中, 只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。
1.3 优点&缺点
优点 | 缺点 |
---|---|
开闭原则。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。 | 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。 |
单一职责原则。 可将同一行为的不同版本移到同一个类中。 | 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。 |
访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。 |
2 Demo-如何使用访问者模式
访问者模式允许你在结构体中添加行为,而又不会对结构体造成实际变更。下面用一个Demo来说明如何使用访问者模式。
2.1 初始状态
现在有一个Shape接口和三个形状结构体,三个结构体实现了接口的Name()函数,在golang中可以认为结构体implement了接口。
package main
import (
"fmt"
"math"
)
type Shape interface {
Name() string
}
type Square struct {
side float64
}
func (s Square) Name() string {
return "square"
}
type Circle struct {
radius float64
}
func (c Circle) Name() string {
return "circle"
}
type Triangle struct {
sideA, sideB, sideC float64
}
func (t Triangle) Name() string {
return "triangle"
}
func main() {
var shapes = []Shape{
Square{1.2},
Circle{1.5},
Triangle{3.0, 4.0, 5.0},
}
for _, shape := range shapes {
fmt.Println(shape.Name())
}
}
现在来了个新需求,需要计算每个图形的面积。一种直接的方式就是向Name()方法一样,在interface Shape
中定义getArea()
,然后每个struct实现getArea()。
2.2 每个结构体都实现一遍新方法
type Shape interface {
Name() string
getArea() float64
}
func (s Square) getArea() float64 {
return s.side * s.side
}
func (c Circle) getArea() float64 {
return math.Pi * c.radius * c.radius
}
func (t Triangle) getArea() float64 {
s := (t.sideA + t.sideB + t.sideC) / 2
return math.Sqrt(s * (s - t.sideA) * (s - t.sideB) * (s - t.sideC))
}
这种方式比较直观,也比较简单,但是可扩展性不高。之后如果再有新需求,比如求个周长之类的,还得再把相关方法实现一遍,同时需要修改现有的interface,还可能会引入安全风险。在版本迭代时,应当确定一个原则:不要改之前的代码,要在之前的基础上进行扩展。
在这种情况下,访问者模式可以很优雅地解决问题。
2.3 用访问者模式进行改造

图是白嫖的,基本意思差不多
- 定义一个访问者接口,声明访问者需要有访问哪些对象的能力
接口声明了一系列以以对象结构的具体元素为参数的访问者方法,这些方法可以理解为访问不同对象的入口,由于golang不支持重载,所以方法名都不一样。在Java这些支持重载的语言中,可以统一用一个方法名。
type Visitor interface {
visitForSquare(*Square) float64
visitForCircle(*Circle) float64
visitForTriangle(*Triangle) float64
}
- Shape接口声明一个方法来接收访问者,相当于对象给访问者开了个门
如果访问者有访问该对象的能力(访问者实现了对该对象操作的方法),就可以进行访问。
所有继承该Shape接口的类去实现这个方法。需要注意,访问者模式必须修改原interface,但是这种修改只需要一次,后面的扩展比如求面积、求周长都可以通过这一个方法进行。
type Shape interface {
Name() string
Accept(Visitor) float64
}
func (s *Square) Accept(v Visitor) float64 {
return v.visitForSquare(s)
}
func (c *Circle) Accept(v Visitor) float64 {
return v.visitForCircle(c)
}
func (t *Triangle) Accept(v Visitor) float64 {
return v.visitForTriangle(t)
}
- 实现具体访问者,同时教会这个访问者如何去访问各个对象
可以定义一个struct/class作为访问者,然后这个访问者去实现访问者接口定义的方法,他就有了访问对象的能力。
type areaCalculator struct {
area float64
}
func (a *areaCalculator) visitForSquare(s *Square) float64 {
return s.side * s.side
}
func (a *areaCalculator) visitForCircle(c *Circle) float64 {
return math.Pi * c.radius * c.radius
}
func (a *areaCalculator) visitForTriangle(t *Triangle) float64 {
s := (t.sideA + t.sideB + t.sideC) / 2
return math.Sqrt(s * (s - t.sideA) * (s - t.sideB) * (s - t.sideC))
}
- 访问者从对象的门里进去,完成访问操作
func main() {
areaCalculator := &areaCalculator{}
var shapes = []Shape{
&Square{1.2},
&Circle{1.5},
&Triangle{3.0, 4.0, 5.0},
}
for _, shape := range shapes {
fmt.Println(shape.Name(), shape.Accept(areaCalculator))
}
}
之后再扩展就非常方便了,只需要定义一个struct/class,然后实现访问者interface声明的所有方法,他就成了一个访问者。不同的是每个访问者实现的方法体具体做什么操作可以定制化。
下面是整体代码
package main
import (
"fmt"
"math"
)
type Shape interface {
Name() string
Accept(Visitor) float64
}
type Square struct {
side float64
}
func (s *Square) Name() string {
return "square"
}
type Circle struct {
radius float64
}
func (c *Circle) Name() string {
return "circle"
}
type Triangle struct {
sideA, sideB, sideC float64
}
func (t *Triangle) Name() string {
return "triangle"
}
func (s *Square) Accept(v Visitor) float64 {
return v.visitForSquare(s)
}
func (c *Circle) Accept(v Visitor) float64 {
return v.visitForCircle(c)
}
func (t *Triangle) Accept(v Visitor) float64 {
return v.visitForTriangle(t)
}
type Visitor interface {
visitForSquare(*Square) float64
visitForCircle(*Circle) float64
visitForTriangle(*Triangle) float64
}
type areaCalculator struct {
area float64
}
func (a *areaCalculator) visitForSquare(s *Square) float64 {
return s.side * s.side
}
func (a *areaCalculator) visitForCircle(c *Circle) float64 {
return math.Pi * c.radius * c.radius
}
func (a *areaCalculator) visitForTriangle(t *Triangle) float64 {
s := (t.sideA + t.sideB + t.sideC) / 2
return math.Sqrt(s * (s - t.sideA) * (s - t.sideB) * (s - t.sideC))
}
func main() {
areaCalculator := &areaCalculator{}
var shapes = []Shape{
&Square{1.2},
&Circle{1.5},
&Triangle{3.0, 4.0, 5.0},
}
for _, shape := range shapes {
fmt.Println(shape.Name(), shape.Accept(areaCalculator))
}
}
3 revive和revive-cc中的访问者模式
3.1 Visitor interface
在go/ast/walk.go:12
定义了Visitor interface
,声明了方法Visit(node Node)
,其中Node也是一个interface。该方法提供了访问Node节点的能力,会被Walk函数用来访问节点。
// go/ast/walk.go:12
// A Visitor's Visit method is invoked for each node encountered by Walk.
// If the result visitor w is not nil, Walk visits each of the children
// of node with the visitor w, followed by a call of w.Visit(nil).
type Visitor interface {
Visit(node Node) (w Visitor)
}
Walk函数
walk函数通过dfs对ast进行遍历,通过visit方法访问ast上的node。
// go/ast/walk.go
// Walk traverses an AST in depth-first order: It starts by calling
// v.Visit(node); node must not be nil. If the visitor w returned by
// v.Visit(node) is not nil, Walk is invoked recursively with visitor
// w for each of the non-nil children of node, followed by a call of
// w.Visit(nil).
//
func Walk(v Visitor, node Node) {
if v = v.Visit(node); v == nil {
return
}
// walk children
// (the order of the cases matches the order
// of the corresponding node types in ast.go)
switch n := node.(type) {
// Comments and fields
case *Comment:
// nothing to do
case *CommentGroup:
for _, c := range n.List {
Walk(v, c)
}
...
...
...
}
3.2 Concrete Visitor
revive-cc这个项目是利用revive
做静态分析和缺陷检测的。主要实现的就是Concrete Visitor
,在rule目录下定义了一系列规则,在规则文件中实现了访问者实体。
如rule/imports-blacklist.go
中实现了接口中声明的visit函数
type blacklistedImports struct {
file *lint.File
fileAst *ast.File
onFailure func(lint.Failure)
blacklist map[string]bool
}
func (w blacklistedImports) Visit(_ ast.Node) ast.Visitor {
for _, is := range w.fileAst.Imports {
if is.Path != nil && !w.file.IsTest() && w.blacklist[is.Path.Value] {
w.onFailure(lint.Failure{
Confidence: 1,
Failure: fmt.Sprintf("should not use the following blacklisted import: %s", is.Path.Value),
Node: is,
Category: "imports",
})
}
}
return nil
}
3.3 Element
被访问的对象元素就是前面提到的Node,他们定义的方法不是之前的accept
。但也只是名字不同,本质应该还是一样的。
Todo:目前还有个问题没搞清楚,为什么interface中声明的函数没有参数,安装之前所学,应该有个Visitor来接收访问者。还在思考,后续补上
// go/ast/ast.go
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface. 表达式:expression
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface. 语句:statement
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface. 声明:declaration
type Decl interface {
Node
declNode()
}
// Pos and End implementations for expression/type nodes.
func (x *BadExpr) Pos() token.Pos { return x.From }
func (x *Ident) Pos() token.Pos { return x.NamePos }
func (x *Ellipsis) Pos() token.Pos { return x.Ellipsis }
func (x *BasicLit) Pos() token.Pos { return x.ValuePos }
// exprNode() ensures that only expression/type nodes can be
// assigned to an Expr.
func (*BadExpr) exprNode() {}
func (*Ident) exprNode() {}
func (*Ellipsis) exprNode() {}
func (*BasicLit) exprNode() {}