Skip to content

访问者模式(Visitor)

前言

访问者模式让你可以在不修改对象结构的前提下,定义作用于这些对象的新操作。它是编译器、AST 解析、文档导出的核心模式。本文将详细讲解访问者模式的核心原理及实际应用。


一、核心概念

1.1 定义

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

1.2 核心角色

角色说明
访问者接口(Visitor)为每种元素定义 visit 方法
具体访问者(ConcreteVisitor)实现具体操作
元素接口(Element)定义 accept 方法,接收访问者
具体元素(ConcreteElement)实现 accept,调用访问者对应方法
对象结构(ObjectStructure)元素集合,可枚举

1.3 双重分派

普通分派:根据对象类型调用方法
  obj.method()  → 根据对象类型决定

双重分派:根据访问者和元素两个类型决定
  element.accept(visitor)
    → visitor.visitConcreteElement(this)
      → 根据元素类型 + 访问者类型共同决定

二、代码实现

2.1 基础实现

php
<?php
// 访问者接口
interface Visitor
{
    public function visitConcreteElementA(ConcreteElementA $element): void;
    public function visitConcreteElementB(ConcreteElementB $element): void;
}

// 元素接口
interface Element
{
    public function accept(Visitor $visitor): void;
    public function operation(): string;
}

// 具体元素 A
class ConcreteElementA implements Element
{
    public function operation(): string
    {
        return "Element A";
    }

    public function operationA(): string
    {
        return "Special A";
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->visitConcreteElementA($this);
    }
}

// 具体元素 B
class ConcreteElementB implements Element
{
    public function operation(): string
    {
        return "Element B";
    }

    public function operationB(): string
    {
        return "Special B";
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->visitConcreteElementB($this);
    }
}

// 具体访问者 1:打印
class PrintVisitor implements Visitor
{
    public function visitConcreteElementA(ConcreteElementA $element): void
    {
        echo "Visitor 1: {$element->operation()} - {$element->operationA()}" . PHP_EOL;
    }

    public function visitConcreteElementB(ConcreteElementB $element): void
    {
        echo "Visitor 1: {$element->operation()} - {$element->operationB()}" . PHP_EOL;
    }
}

// 具体访问者 2:计数
class CountVisitor implements Visitor
{
    public int $countA = 0;
    public int $countB = 0;

    public function visitConcreteElementA(ConcreteElementA $element): void
    {
        $this->countA++;
    }

    public function visitConcreteElementB(ConcreteElementB $element): void
    {
        $this->countB++;
    }

    public function getTotal(): int
    {
        return $this->countA + $this->countB;
    }
}

// 使用
$elements = [
    new ConcreteElementA(),
    new ConcreteElementB(),
    new ConcreteElementA(),
    new ConcreteElementB(),
    new ConcreteElementA(),
];

$printVisitor = new PrintVisitor();
foreach ($elements as $element) {
    $element->accept($printVisitor);
}

$countVisitor = new CountVisitor();
foreach ($elements as $element) {
    $element->accept($countVisitor);
}
echo "A: {$countVisitor->countA}, B: {$countVisitor->countB}, Total: {$countVisitor->getTotal()}" . PHP_EOL;

2.2 实际案例:形状计算

php
<?php
// 形状接口
interface Shape
{
    public function accept(ShapeVisitor $visitor): void;
}

// 圆形
class Circle implements Shape
{
    public function __construct(public int $radius) {}

    public function accept(ShapeVisitor $visitor): void
    {
        $visitor->visitCircle($this);
    }
}

// 矩形
class Rectangle implements Shape
{
    public function __construct(
        public int $width,
        public int $height
    ) {}

    public function accept(ShapeVisitor $visitor): void
    {
        $visitor->visitRectangle($this);
    }
}

// 三角形
class Triangle implements Shape
{
    public function __construct(
        public int $base,
        public int $height
    ) {}

    public function accept(ShapeVisitor $visitor): void
    {
        $visitor->visitTriangle($this);
    }
}

// 访问者接口
interface ShapeVisitor
{
    public function visitCircle(Circle $circle): void;
    public function visitRectangle(Rectangle $rect): void;
    public function visitTriangle(Triangle $tri): void;
}

// 面积计算访问者
class AreaCalculatorVisitor implements ShapeVisitor
{
    public float $totalArea = 0;

    public function visitCircle(Circle $circle): void
    {
        $area = M_PI * $circle->radius ** 2;
        echo "Circle area: " . number_format($area, 2) . PHP_EOL;
        $this->totalArea += $area;
    }

    public function visitRectangle(Rectangle $rect): void
    {
        $area = $rect->width * $rect->height;
        echo "Rectangle area: {$area}" . PHP_EOL;
        $this->totalArea += $area;
    }

    public function visitTriangle(Triangle $tri): void
    {
        $area = 0.5 * $tri->base * $tri->height;
        echo "Triangle area: {$area}" . PHP_EOL;
        $this->totalArea += $area;
    }
}

// 周长计算访问者
class PerimeterCalculatorVisitor implements ShapeVisitor
{
    public float $totalPerimeter = 0;

    public function visitCircle(Circle $circle): void
    {
        $perimeter = 2 * M_PI * $circle->radius;
        echo "Circle perimeter: " . number_format($perimeter, 2) . PHP_EOL;
        $this->totalPerimeter += $perimeter;
    }

    public function visitRectangle(Rectangle $rect): void
    {
        $perimeter = 2 * ($rect->width + $rect->height);
        echo "Rectangle perimeter: {$perimeter}" . PHP_EOL;
        $this->totalPerimeter += $perimeter;
    }

    public function visitTriangle(Triangle $tri): void
    {
        // 假设等边三角形
        $perimeter = 3 * $tri->base;
        echo "Triangle perimeter: {$perimeter}" . PHP_EOL;
        $this->totalPerimeter += $perimeter;
    }
}

// XML 导出访问者
class XmlExportVisitor implements ShapeVisitor
{
    public string $xml = "";

    public function visitCircle(Circle $circle): void
    {
        $this->xml .= "<circle radius=\"{$circle->radius}\"/>\n";
    }

    public function visitRectangle(Rectangle $rect): void
    {
        $this->xml .= "<rectangle width=\"{$rect->width}\" height=\"{$rect->height}\"/>\n";
    }

    public function visitTriangle(Triangle $tri): void
    {
        $this->xml .= "<triangle base=\"{$tri->base}\" height=\"{$tri->height}\"/>\n";
    }
}

// 使用:同一组形状,不同操作
$shapes = [
    new Circle(5),
    new Rectangle(4, 6),
    new Triangle(3, 4),
];

$areaCalc = new AreaCalculatorVisitor();
foreach ($shapes as $shape) {
    $shape->accept($areaCalc);
}
echo "Total area: " . number_format($areaCalc->totalArea, 2) . PHP_EOL . PHP_EOL;

$xmlExport = new XmlExportVisitor();
foreach ($shapes as $shape) {
    $shape->accept($xmlExport);
}
echo $xmlExport->xml;

2.3 实际案例:AST 遍历

php
<?php
// AST 节点接口
interface ASTNode
{
    public function accept(ASTVisitor $visitor): void;
}

// 数字字面量
class NumberLiteral implements ASTNode
{
    public function __construct(public int $value) {}

    public function accept(ASTVisitor $visitor): void
    {
        $visitor->visitNumberLiteral($this);
    }
}

// 二元表达式
class BinaryExpression implements ASTNode
{
    public function __construct(
        public ASTNode $left,
        public string $operator,
        public ASTNode $right
    ) {}

    public function accept(ASTVisitor $visitor): void
    {
        $visitor->visitBinaryExpression($this);
    }
}

// 变量引用
class Variable implements ASTNode
{
    public function __construct(public string $name) {}

    public function accept(ASTVisitor $visitor): void
    {
        $visitor->visitVariable($this);
    }
}

// 访问者接口
interface ASTVisitor
{
    public function visitNumberLiteral(NumberLiteral $node): void;
    public function visitBinaryExpression(BinaryExpression $node): void;
    public function visitVariable(Variable $node): void;
}

// 解释器访问者:求值
class InterpreterVisitor implements ASTVisitor
{
    /** @var int[] */
    private array $stack = [];
    /** @var array<string, int> */
    private array $variables = ["x" => 10, "y" => 20];

    public function visitNumberLiteral(NumberLiteral $node): void
    {
        $this->stack[] = $node->value;
    }

    public function visitVariable(Variable $node): void
    {
        $val = $this->variables[$node->name] ?? null;
        if ($val === null) throw new Error("Unknown variable: {$node->name}");
        $this->stack[] = $val;
    }

    public function visitBinaryExpression(BinaryExpression $node): void
    {
        $node->left->accept($this);
        $node->right->accept($this);
        $right = array_pop($this->stack);
        $left = array_pop($this->stack);
        $result = match ($node->operator) {
            "+" => $left + $right,
            "-" => $left - $right,
            "*" => $left * $right,
            "/" => $left / $right,
            default => throw new Error("Unknown operator: {$node->operator}"),
        };
        $this->stack[] = $result;
    }

    public function getResult(): int
    {
        return $this->stack[0];
    }
}

// 打印访问者:还原表达式
class PrinterVisitor implements ASTVisitor
{
    private string $buffer = "";

    public function visitNumberLiteral(NumberLiteral $node): void
    {
        $this->buffer .= $node->value;
    }

    public function visitVariable(Variable $node): void
    {
        $this->buffer .= $node->name;
    }

    public function visitBinaryExpression(BinaryExpression $node): void
    {
        $this->buffer .= "(";
        $node->left->accept($this);
        $this->buffer .= " {$node->operator} ";
        $node->right->accept($this);
        $this->buffer .= ")";
    }

    public function getExpression(): string
    {
        return $this->buffer;
    }
}

// 使用
// 表达式:(x + 5) * (y - 3)
$ast = new BinaryExpression(
    new BinaryExpression(new Variable("x"), "+", new NumberLiteral(5)),
    "*",
    new BinaryExpression(new Variable("y"), "-", new NumberLiteral(3)),
);

$interpreter = new InterpreterVisitor();
$ast->accept($interpreter);
echo "Result: {$interpreter->getResult()}" . PHP_EOL; // (10 + 5) * (20 - 3) = 255

$printer = new PrinterVisitor();
$ast->accept($printer);
echo "Expression: {$printer->getExpression()}" . PHP_EOL; // ((x + 5) * (y - 3))

三、适用场景

场景说明
编译器AST 遍历、类型检查
文档处理不同格式导出
数据结构多种操作
报表生成不同维度统计
静态分析代码检查、Linter
模式匹配复杂条件分发

四、优缺点分析

优点缺点
新增操作简单新增元素类型困难
符合单一职责违反依赖倒置
积累状态强耦合元素和访问者
灵活扩展操作双重分派复杂
集中相关操作需暴露元素内部

五、常见踩坑与问题排查

5.1 新增元素类型困难

php
<?php
// 问题:新增 Triangle 后,所有访问者都要修改
interface ShapeVisitor
{
    public function visitCircle(Circle $c): void;
    public function visitRectangle(Rectangle $r): void;
    public function visitTriangle(Triangle $t): void; // 新增!所有访问者都要实现
}

// 解决:如果元素类型频繁变化,考虑使用其他模式(如策略)

5.2 访问者状态管理

php
<?php
// 问题:访问者复用时状态未清理
class BadVisitor implements Visitor
{
    public int $total = 0;

    public function visitCircle(): void
    {
        $this->total += 1;
    }
    // 再次使用时 total 未重置
}

// 解决:每次使用创建新实例,或提供 reset 方法
class GoodVisitor implements Visitor
{
    public int $total = 0;

    public function reset(): void
    {
        $this->total = 0;
    }

    public function visitCircle(): void
    {
        $this->total += 1;
    }
}

5.3 循环引用

php
<?php
// 问题:元素间循环引用导致无限递归
class Node
{
    /** @var Node[] */
    private array $children = [];

    public function accept(Visitor $visitor): void
    {
        $visitor->visitNode($this);
        foreach ($this->children as $child) {
            $child->accept($visitor); // 可能循环
        }
    }
}

// 解决:使用 visited 集合检测
class SafeVisitor implements Visitor
{
    /** @var array<string, bool> */
    private array $visited = [];

    public function visitNode(Node $node): void
    {
        $key = spl_object_id($node);
        if (isset($this->visited[$key])) return;
        $this->visited[$key] = true;
        // 处理
    }
}

六、优化方案与进阶

6.1 函数式访问者

php
<?php
// 使用回调数组代替接口
function visit(Shape $shape, array $handlers): void
{
    $className = get_class($shape);
    $method = "visit{$className}";
    if (isset($handlers[$method]) && is_callable($handlers[$method])) {
        $handlers[$method]($shape);
    }
}

// 使用
foreach ($shapes as $shape) {
    visit($shape, [
        "visitCircle" => function(Circle $c) {
            echo "Circle: {$c->radius}" . PHP_EOL;
        },
        "visitRectangle" => function(Rectangle $r) {
            echo "Rect: {$r->width}x{$r->height}" . PHP_EOL;
        },
    ]);
}

6.2 反射式访问者

php
<?php
// PHP 使用类名自动分发
class ReflectiveVisitor
{
    public function visit($element): void
    {
        $methodName = "visit" . get_class($element);
        if (method_exists($this, $methodName)) {
            $this->$methodName($element);
        } else {
            $this->visitDefault($element);
        }
    }

    public function visitDefault($element): void
    {
        echo "Unknown: " . get_class($element) . PHP_EOL;
    }
}

class MyVisitor extends ReflectiveVisitor
{
    public function visitCircle(Circle $c): void
    {
        echo "Circle: {$c->radius}" . PHP_EOL;
    }

    public function visitRectangle(Rectangle $r): void
    {
        echo "Rect: {$r->width}x{$r->height}" . PHP_EOL;
    }
}

七、全文总结

访问者模式的核心是 在不修改元素类的前提下,新增作用于元素结构的操作

核心要点

  1. 通过双重分派根据元素类型调用对应方法
  2. 适用于元素结构稳定但操作多变的场景
  3. 新增操作容易,但新增元素类型困难
  4. 是编译器 AST 遍历、文档导出的经典模式
  5. 可用函数式对象或反射简化实现