新增操作时不能改Element接口,因其违反开闭原则;应新增Visitor实现类(如MarkdownVisitor),通过VisitFile/VisitDirectory方法封装逻辑,保持Element结构稳定。

新增操作时为什么不能改 Element 接口
访问者模式的核心契约是:Element 接口只定义 Accept(visitor Visitor) 方法,所有具体元素(如 File、Directory)都必须实现它。一旦你为某个具体元素类型新增一个操作(比如「计算校验和」),却去修改 File 或 Directory 的结构(例如加新方法),就破坏了开闭原则——后续每加一个操作都要动已有类,可维护性立刻崩塌。
真正该做的,是新增一个实现了 Visitor 接口的新类型,比如 ChecksumVisitor,然后在它的 VisitFile 和 VisitDirectory 方法里写逻辑。
Visitor 接口如何设计才支持无侵入扩展
关键在于:Visitor 接口的方法签名必须覆盖全部 Element 类型,且方法名要能体现“被访问对象”的语义,而不是操作意图。否则每次加新 Element 类型(比如加个 Symlink),就得改 Visitor 接口,又违反开闭。
type Visitor interface {
VisitFile(*File)
VisitDirectory(*Directory)
// 不要叫 VisitForBackup() 或 VisitForLog() —— 这是操作意图,会随业务变
// 也不要漏掉未来可能的类型,宁可预留 VisitSymlink(*Symlink) 并空实现
}
常见错误:
立即学习“go语言免费学习笔记(深入)”;
- 把
Visitor设计成泛型接口(如Visitor[T any]),导致每个新操作都要声明新接口,无法统一调度 - 用反射动态调用 Visit 方法——失去编译期检查,运行时报错难定位
- 在
Accept里硬编码 switch type,绕过 Visitor 接口,等于没用模式
实际新增一个操作:生成 Markdown 目录树
假设已有 File 和 Directory,现在要支持「输出为 Markdown 格式目录结构」。不改任何现有 Element 实现,只加:
技术上面应用了三层结构,AJAX框架,URL重写等基础的开发。并用了动软的代码生成器及数据访问类,加进了一些自己用到的小功能,算是整理了一些自己的操作类。系统设计上面说不出用什么模式,大体设计是后台分两级分类,设置好一级之后,再设置二级并选择栏目类型,如内容,列表,上传文件,新窗口等。这样就可以生成无限多个二级分类,也就是网站栏目。对于扩展性来说,如果有新的需求可以直接加一个栏目类型并新加功能操作
type MarkdownVisitor struct {
buf strings.Builder
indent int
}
func (v MarkdownVisitor) VisitFile(f File) {
v.buf.WriteString(strings.Repeat(" ", v.indent))
v.buf.WriteString("- ? ")
v.buf.WriteString(f.Name)
v.buf.WriteString("\n")
}
func (v MarkdownVisitor) VisitDirectory(d Directory) {
v.buf.WriteString(strings.Repeat(" ", v.indent))
v.buf.WriteString("- ? ")
v.buf.WriteString(d.Name)
v.buf.WriteString("\n")
v.indent++
for _, child := range d.Children {
child.Accept(v) // 注意:这里依赖每个 child 都实现了 Accept
}
v.indent--
}
func (v *MarkdownVisitor) Result() string {
return v.buf.String()
}
调用方式干净利落:
visitor := &MarkdownVisitor{}
root.Accept(visitor)
fmt.Println(visitor.Result())
这个过程没碰 File、Directory、Visitor 接口一行代码。
容易被忽略的边界:循环引用与访问深度控制
真实文件系统可能有符号链接甚至硬链接环,Accept 递归调用时若不做防环处理,会栈溢出。这不是 Visitor 模式本身的问题,但扩展新 Visitor 时极易忽略。
建议在 Visitor 实现里自带访问路径记录(比如 map[uintptr]bool 记住已访问对象地址),或传入上下文控制最大深度:
- 在
VisitDirectory开头加if depth > maxDepth { return } - 避免在
Accept方法里直接递归调用自身,而是由 Visitor 主动决定是否深入子节点 - 如果 Element 结构本身支持 ID(如 inode),Visitor 可以用
map[uint64]bool去重
没有统一的「安全访问器基类」,每个新 Visitor 都得自己考虑这点——这是扩展性代价中最容易漏掉的一块。









