访问者模式适用于元素结构稳定且操作频繁变化的场景,核心是通过Visitor接口将操作与数据分离,但要求接口预先覆盖所有元素类型,否则新增操作需修改原有代码。

能实现,但前提是元素结构已稳定、访问者接口已覆盖所有元素类型。否则新增操作反而要动老代码,得不偿失。
Visitor 接口必须提前声明所有元素类型的 Visit 方法
Go 或 Java 中,Visitor 是一个接口,它不是泛型或动态的——每个具体元素类(如 File、Directory、Image)都对应一个固定方法,比如 VisitFile(*File)、VisitDirectory(*Directory)。一旦漏掉某个类型,新访问者就无法处理它;后续若新增元素类型(如 Symlink),就必须修改 Visitor 接口,连带所有已存在的 ConcreteVisitor 实现都要补方法。
- 接口定义越早收口、覆盖越全,后期新增操作才真正“只加不改”
- Python 用
isinstance()或functools.singledispatch可绕过显式接口,但失去编译期检查和 IDE 支持 - C++ 模板 + ADL 或
std::visit配合std::variant更灵活,但要求数据结构本身是封闭枚举式
每个 ConcreteElement 的 Accept 方法必须正确调用对应 VisitXXX
Accept 不是装饰器或钩子,它是硬编码的分发逻辑。比如 File.Accept(v) 必须写死为 v.VisitFile(f),不能靠反射或字符串拼接。这意味着:如果某天你想让 File 同时被当成普通文件和可执行文件处理,就得拆成两个类型,或在 VisitFile 内部分支——但分支逻辑又回到了“改访问者”,没解决扩展性问题。
- Accept 方法体越简单越好,只做一次方法跳转,别塞业务判断
- 不要在
Accept里做递归遍历(如Directory.Accept自己遍历子节点),应交给访问者控制遍历顺序和时机 - 如果元素有嵌套结构(如
Directory包含[]Element),它的Accept仍只调用v.VisitDirectory(d),遍历子节点是访问者自己的事
新增操作 = 新建 ConcreteVisitor,且不能依赖未公开字段
比如你要加一个 MarkdownExportVisitor,它必须只通过 VisitFile 和 VisitDirectory 暴露的公共字段(如 Name、Size)生成内容。如果 File 内部有个未导出的 contentHash 字段,而导出 Markdown 又需要它——那就只能改 File 加 Getter,或者把逻辑挪到别处。访问者模式不解决封装边界问题,它只解决“调用入口”的解耦。
- 访问者天然需要了解元素的内部结构,否则无法做有意义的操作
- 字段暴露程度决定了访问者的实际能力边界,设计初期就要权衡:哪些该公开,哪些该藏进方法里
- 如果多个访问者反复需要同一组衍生数据(如“路径全名”),建议在元素上提供
FullPath()这类只读方法,而非让每个访问者自己拼
最易被忽略的一点:访问者模式不是万能胶。当操作之间开始共享状态(比如统计+导出要共用缓存)、或需要跨元素协调(比如“找出所有大于 10MB 的文件并高亮其父目录”),你就得在访问者里维护上下文,甚至引入回调或事件机制——这时模式本身就开始变重,不如直接重构数据结构或换用策略+组合。

















