Visitor 模式在 PHP 中靠接口约定和双重分派手动实现,适用于对象结构稳定但操作频繁新增的场景;因 PHP 不支持方法重载,需元素显式调用 $visitor->visitUser($this) 等类型特化方法,参数必须声明具体类型以保障类型安全与工具支持。

Visitor 模式在 PHP 中不是靠语言特性支持的,而是靠接口约定 + 双重分派(double dispatch)手动实现的。它适合对象结构稳定、但操作频繁新增的场景——比如你有一套已上线的 User、Group、Permission 类,现在要加日志、导出、校验、序列化等新逻辑,又不想改原有类。
为什么 PHP 的 accept() 必须显式调用访问者方法?
PHP 不支持方法重载(overload),也没有基于参数类型的动态分派机制。所以不能像 Java 那样写 $visitor->visit($element) 然后由运行时自动选中 visit(User $u) 还是 visit(Group $g)。
必须让每个具体元素自己决定调用哪个访问方法:$visitor->visitUser($this) 或 $visitor->visitGroup($this)。这就是“双重分派”的手动落地:第一层是元素调用访问者,第二层是访问者根据类型执行对应逻辑。
Visitor 接口定义时,方法签名必须严格匹配具体元素类型
常见错误是把所有 visitXxx() 方法参数都写成 Element $e,导致类型信息丢失,后续无法做类型安全操作(比如调用 $e->getUsername() 会报错)。
- 正确做法:每个方法参数明确声明具体类型,如
visitUser(User $user)、visitGroup(Group $group) - 这样访问者内部就能直接使用
$user->getEmail(),IDE 也能补全,静态分析工具(如 PHPStan)可校验 - 如果未来新增
Role类,就要同步在Visitor接口中加visitRole(Role $role),否则所有实现类都会报错——这是强制你保持契约一致的手段
对象结构(ObjectStructure)不是必需的,但能简化批量访问
很多教程硬塞一个 ObjectStructure 类,其实它只是个容器+遍历器。你完全可以用原生数组替代:
$elements = [$user1, $groupA, $user2];
foreach ($elements as $e) {
$e->accept($recordingVisitor);
}
只有当你需要支持增删元素、延迟加载、过滤或嵌套结构(比如 Group 内含子 Group)时,才值得封装成类。否则徒增抽象,反而让调用方多一层理解成本。
立即学习“PHP免费学习笔记(深入)”;
实际项目中容易被忽略的复杂点
访问者模式真正难的不是写代码,而是判断“这个需求到底适不适合用 Visitor”:
- 当某个操作只涉及 1–2 个元素类型,且逻辑简单(比如只读取
User的邮箱发通知),直接写个函数更清晰,别硬套 Visitor - 当元素之间有强依赖(比如
Group::accept()里要递归调用子User的accept()),那你要确保访问者能处理嵌套层级,否则容易栈溢出或漏访问 - 如果访问者需要修改元素状态(比如标记“已导出”),PHP 的传值/传引用行为要特别小心——
$user是对象引用,但你在visitUser()里改属性是生效的;可如果误用了clone或重新赋值变量,就可能失效



















