访问者模式(Visitor Pattern)
访问者模式(Visitor Pattern)
1. 知识点总结
访问者模式是一种行为型设计模式,核心思想是:
把“作用于对象结构上的操作”从对象本身剥离出来,使得在不修改原有元素类的前提下,可以灵活地新增操作。
在 Go 中,由于没有传统 OOP 的类继承,访问者模式通常用 接口 + 多态分发 来实现:
- 被访问的元素声明
Accept(Visitor)方法; - 访问者接口为每一种元素类型声明一个
VisitXxx方法; - 新增操作时,只需新增一个访问者实现,无需改动元素类。
适用场景
- 对象结构相对稳定,但需要在其上频繁新增业务操作;
- 希望对一组异构对象执行统一处理,例如统计、校验、导出;
- 不想把各种“边缘操作”污染到核心业务对象中。
优缺点
| 优点 | 缺点 |
|---|---|
| 符合开闭原则:新增操作不必改元素类 | 元素类型增加时,所有访问者都要新增对应方法 |
| 把不同操作集中到独立的访问者中,职责清晰 | 访问者需要了解元素的内部结构,可能破坏封装 |
| 便于对异构对象做统一遍历和聚合 | 对于简单场景不如直接 switch type 或接口方法简洁 |
2. 示例代码
下面用购物车来演示:购物车里有图书、水果、电子产品三种商品。我们用不同的访问者分别计算原始总价、折后总价和商品名称列表。
package main
import "fmt"
// Item 是被访问的元素接口,代表购物车中的商品。
type Item interface {
Accept(Visitor)
}
// Visitor 声明可以在不同商品上执行的操作集合。
type Visitor interface {
VisitBook(*Book)
VisitFruit(*Fruit)
VisitElectronic(*Electronic)
}
// Book 是具体元素:图书。
type Book struct {
Name string
Price float64
}
func (b *Book) Accept(v Visitor) { v.VisitBook(b) }
// Fruit 是具体元素:水果(按重量计价)。
type Fruit struct {
Name string
PricePerKg float64
WeightKg float64
}
func (f *Fruit) Accept(v Visitor) { v.VisitFruit(f) }
// Electronic 是具体元素:电子产品。
type Electronic struct {
Name string
Price float64
}
func (e *Electronic) Accept(v Visitor) { v.VisitElectronic(e) }
// PriceVisitor 计算购物车原始总价。
type PriceVisitor struct {
Total float64
}
func (p *PriceVisitor) VisitBook(b *Book) { p.Total += b.Price }
func (p *PriceVisitor) VisitFruit(f *Fruit) { p.Total += f.PricePerKg * f.WeightKg }
func (p *PriceVisitor) VisitElectronic(e *Electronic) { p.Total += e.Price }
// DiscountVisitor 计算打折后的总价,不同品类折扣不同。
type DiscountVisitor struct {
Total float64
}
func (d *DiscountVisitor) VisitBook(b *Book) { d.Total += b.Price * 0.9 }
func (d *DiscountVisitor) VisitFruit(f *Fruit) { d.Total += f.PricePerKg * f.WeightKg * 0.95 }
func (d *DiscountVisitor) VisitElectronic(e *Electronic) { d.Total += e.Price * 0.85 }
// NameVisitor 收集所有商品名称。
type NameVisitor struct {
Names []string
}
func (n *NameVisitor) VisitBook(b *Book) { n.Names = append(n.Names, b.Name) }
func (n *NameVisitor) VisitFruit(f *Fruit) { n.Names = append(n.Names, f.Name) }
func (n *NameVisitor) VisitElectronic(e *Electronic) { n.Names = append(n.Names, e.Name) }
func main() {
cart := []Item{
&Book{Name: "Go语言圣经", Price: 89.0},
&Fruit{Name: "苹果", PricePerKg: 12.0, WeightKg: 2.5},
&Electronic{Name: "机械键盘", Price: 399.0},
}
price := &PriceVisitor{}
discount := &DiscountVisitor{}
names := &NameVisitor{}
for _, item := range cart {
item.Accept(price)
item.Accept(discount)
item.Accept(names)
}
fmt.Printf("商品列表: %v\n", names.Names)
fmt.Printf("原始总价: %.2f\n", price.Total)
fmt.Printf("折后总价: %.2f\n", discount.Total)
}
3. 运行结果
商品列表: [Go语言圣经 苹果 机械键盘]
原始总价: 518.00
折后总价: 447.75
4. 关键点复盘
- 双分派(Double Dispatch):
item.Accept(visitor)先根据元素类型选择Accept实现,再根据访问者类型选择VisitXxx方法。这是访问者模式的核心机制。 - 元素类保持稳定:如果以后新增“满减计算”“发票打印”等操作,只需要新增访问者,图书、水果、电子产品的代码完全不用动。
- Go 实现要点:通过接口把元素和访问者解耦;访问者的方法命名建议带元素类型前缀,避免方法冲突。
- 注意权衡:当元素类型经常变化时,访问者模式维护成本会上升,这时可能更适合用
type switch或策略模式。

浙公网安备 33010602011771号