22. 访问者模式(Visitor)

问题:想在不修改对象结构的前提下,为其增加新操作。

核心:把操作从对象结构中分离出来,放到访问者中。

interface Visitor<R> {
  visitNumber(node: NumberNode): R;
  visitString(node: StringNode): R;
}

interface AstNode {
  accept<R>(visitor: Visitor<R>): R;
}

class NumberNode implements AstNode {
  constructor(public readonly value: number) {}
  accept<R>(visitor: Visitor<R>): R { return visitor.visitNumber(this); }
}

class StringNode implements AstNode {
  constructor(public readonly value: string) {}
  accept<R>(visitor: Visitor<R>): R { return visitor.visitString(this); }
}

class TypeVisitor implements Visitor<string> {
  visitNumber(node: NumberNode): string { return `数字: ${node.value}`; }
  visitString(node: StringNode): string { return `字符串: "${node.value}"`; }
}

class LengthVisitor implements Visitor<number> {
  visitNumber(node: NumberNode): number { return String(node.value).length; }
  visitString(node: StringNode): number { return node.value.length; }
}

const nodes: AstNode[] = [new NumberNode(42), new StringNode('hello')];

nodes.forEach(n => console.log(n.accept(new TypeVisitor())));
// 数字: 42
// 字符串: "hello"

nodes.forEach(n => console.log(n.accept(new LengthVisitor())));
// 2
// 5
package visitor

import (
  "fmt"
  "strconv"
)

type Visitor interface {
  VisitNumber(n *NumberNode) string
  VisitString(s *StringNode) string
}

type Node interface {
  Accept(v Visitor) string
}

type NumberNode struct{ Value int }

func (n *NumberNode) Accept(v Visitor) string { return v.VisitNumber(n) }

type StringNode struct{ Value string }

func (s *StringNode) Accept(v Visitor) string { return v.VisitString(s) }

type TypeVisitor struct{}

func (t TypeVisitor) VisitNumber(n *NumberNode) string {
  return fmt.Sprintf("数字: %d", n.Value)
}

func (t TypeVisitor) VisitString(s *StringNode) string {
  return fmt.Sprintf("字符串: \"%s\"", s.Value)
}

type LengthVisitor struct{}

func (l LengthVisitor) VisitNumber(n *NumberNode) string {
  return strconv.Itoa(len(strconv.Itoa(n.Value)))
}

func (l LengthVisitor) VisitString(s *StringNode) string {
  return strconv.Itoa(len(s.Value))
}

实战场景

  • AST 分析:语法树节点稳定,但要不断增加类型检查、格式化、代码生成、lint 规则等操作。
  • 文档导出:Markdown 或富文本节点结构不变,可以新增 HTML、PDF、纯文本、摘要等导出 visitor。
  • 表单 schema 处理:同一套字段结构需要渲染、校验、生成默认值、生成文档、统计字段数量。
  • 图形对象统计:图形类型稳定,但可以增加面积计算、碰撞检测、导出 SVG、生成缩略图等操作。
  • 权限审计:资源树结构稳定,新增审计 visitor 遍历每个资源并收集风险点。
  • 搜索索引构建:遍历内容树,为不同节点提取标题、正文、标签、附件文本。
  • 序列化:对象结构稳定时,为 JSON、XML、二进制协议增加不同序列化 visitor。
  • 规则引擎解释:表达式节点不变,但可以增加求值、打印、优化、依赖收集等操作。
  • UI 可访问性检查:遍历组件树,检查缺失 label、颜色对比度、键盘交互等问题。
  • 编译优化:编译器对 AST 增加常量折叠、死代码删除、作用域分析等 visitor。