AIGC标识 访问者模式(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. 关键点复盘

  1. 双分派(Double Dispatch)item.Accept(visitor) 先根据元素类型选择 Accept 实现,再根据访问者类型选择 VisitXxx 方法。这是访问者模式的核心机制。
  2. 元素类保持稳定:如果以后新增“满减计算”“发票打印”等操作,只需要新增访问者,图书、水果、电子产品的代码完全不用动。
  3. Go 实现要点:通过接口把元素和访问者解耦;访问者的方法命名建议带元素类型前缀,避免方法冲突。
  4. 注意权衡:当元素类型经常变化时,访问者模式维护成本会上升,这时可能更适合用 type switch 或策略模式。
posted @ 2026-08-07 09:04  FfHUCisI  阅读(5)  评论(0)    收藏  举报