Skip to content

访问者模式

引言

在软件设计中,当需要对复杂对象结构(如文档树、抽象语法树)添加新操作时,直接修改元素类会导致代码臃肿开闭原则破坏。访问者模式通过双重分发机制(Double Dispatch),将操作从数据结构中分离,实现“操作可插拔”,成为处理异构对象遍历的经典方案。

诞生背景

GoF在《设计模式》中提出访问者模式,解决三大痛点:

  • 操作污染:频繁为对象结构添加新操作(如文档导出、格式检查)需修改所有元素类
  • 类型耦合:遍历代码需大量instanceof判断(如处理XML节点时区分<text>/<image>
  • 复用困难:相似操作(如PDF/HTML导出)无法复用核心遍历逻辑

演进过程

  • GoF基础(1994):确立核心角色(Visitor、Element)
  • 语言演进:Java/C#等通过方法重载支持静态双分派
  • 现代应用:编译器设计(AST遍历)、AI行为树处理

核心概念

  • 访问者(Visitor):声明visit(element)接口,定义可执行的操作集合
  • 具体访问者(ConcreteVisitor):实现特定操作(如导出PDF、数据校验)
  • 元素(Element):定义accept(visitor)方法,调用访问者的visit()
  • 对象结构(Object Structure):元素的集合(如文档树、UI组件树)

通用实现

Java 实现

java
// 元素接口
interface DocumentElement {
    void accept(Visitor visitor);
}

// 具体元素:文本段落
class TextElement implements DocumentElement {
    public String content;
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 具体元素:图片
class ImageElement implements DocumentElement {
    public String url;
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 访问者接口
interface Visitor {
    void visit(TextElement text);
    void visit(ImageElement image);
}

// 具体访问者:HTML导出
class HtmlExportVisitor implements Visitor {
    public void visit(TextElement text) {
        System.out.println("<p>" + text.content + "</p>");
    }
    public void visit(ImageElement image) {
        System.out.println("<img src='" + image.url + "'/>");
    }
}

// 对象结构:文档
class Document {
    private List<DocumentElement> elements = new ArrayList<>();
    
    public void addElement(DocumentElement e) {
        elements.add(e);
    }
    
    public void export(Visitor visitor) {
        for (DocumentElement e : elements) {
            e.accept(visitor);
        }
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        Document doc = new Document();
        doc.addElement(new TextElement("Hello World"));
        doc.addElement(new ImageElement("pic.jpg"));
        
        doc.export(new HtmlExportVisitor()); 
        // 输出: <p>Hello World</p> <img src='pic.jpg'/>
    }
}

PHP 实现

php
// 元素接口
interface DocumentElement {
    public function accept(Visitor $visitor);
}

// 具体元素:文本段落
class TextElement implements DocumentElement {
    public $content;
    public function accept(Visitor $visitor) {
        $visitor->visitText($this);
    }
}

// 访问者接口
interface Visitor {
    public function visitText(TextElement $text);
    public function visitImage(ImageElement $image);
}

// 具体访问者:PDF导出
class PdfExportVisitor implements Visitor {
    public function visitText(TextElement $text) {
        echo "PDF Text: " . $text->content . "\n";
    }
    public function visitImage(ImageElement $image) {
        echo "PDF Image: " . $image->url . "\n";
    }
}

// 对象结构
class Document {
    private array $elements = [];
    
    public function addElement(DocumentElement $element): void {
        $this->elements[] = $element;
    }
    
    public function export(Visitor $visitor): void {
        foreach ($this->elements as $element) {
            $element->accept($visitor);
        }
    }
}

// 客户端
$doc = new Document();
$doc->addElement(new TextElement("PHP is great!"));
$doc->export(new PdfExportVisitor());
// 输出: PDF Text: PHP is great!

应用场景

  • 编译器设计:AST遍历(语法检查、代码优化)
  • 文档处理:多格式导出(HTML/PDF/Markdown)
  • UI框架:组件树渲染与事件处理
  • 游戏开发:实体行为计算(伤害、碰撞检测)

案例:购物车价格计算器

Java 实现

java
// 访问者接口
interface PriceVisitor {
    double visit(BookItem book);
    double visit(FruitItem fruit);
}

// 具体访问者:折扣计算
class DiscountVisitor implements PriceVisitor {
    public double visit(BookItem book) {
        return book.price * 0.9; // 图书9折
    }
    public double visit(FruitItem fruit) {
        return fruit.price * fruit.weight; // 水果按重量计费
    }
}

// 元素接口
interface CartItem {
    double accept(PriceVisitor visitor);
}

// 具体元素:图书
class BookItem implements CartItem {
    public double price;
    public double accept(PriceVisitor visitor) {
        return visitor.visit(this);
    }
}

// 客户端
CartItem[] items = {new BookItem(50.0), new FruitItem(5.0, 2.0)};
PriceVisitor visitor = new DiscountVisitor();
double total = 0;
for (CartItem item : items) {
    total += item.accept(visitor);
}
System.out.println("Total: $" + total); // Total: $50*0.9 + 5*2 = $50

PHP 实现

php
// 访问者接口
interface PriceVisitor {
    public function visitBook(BookItem $book): float;
    public function visitFruit(FruitItem $fruit): float;
}

// 具体访问者:税费计算
class TaxVisitor implements PriceVisitor {
    public function visitBook(BookItem $book): float {
        return $book->price * 1.05; // 图书加5%税
    }
    public function visitFruit(FruitItem $fruit): float {
        return $fruit->price * $fruit->weight * 1.1; // 水果加10%税
    }
}

// 具体元素:水果
class FruitItem implements CartItem {
    public float $price;
    public float $weight;
    public function accept(PriceVisitor $visitor): float {
        return $visitor->visitFruit($this);
    }
}

// 客户端
$items = [new BookItem(100), new FruitItem(10, 2)];
$visitor = new TaxVisitor();
$total = 0;
foreach ($items as $item) {
    $total += $item->accept($visitor);
}
echo "Total: $$total"; // Total: $100*1.05 + 10*2*1.1 = $127

优点

  • 开闭原则:新增操作无需修改元素类
  • 高内聚:相关操作集中到访问者类
  • 复用性:多个访问者可复用同一对象结构
  • 复杂操作:跨元素状态的操作更易实现(如购物车总价)

缺点

  • 元素变更成本高:新增元素类型需修改所有访问者
  • 破坏封装:访问者需访问元素内部状态
  • 遍历依赖:对象结构需提供稳定遍历接口
  • 过度设计:简单对象结构使用反而复杂化

扩展

  • 迭代器+访问者
java
for (Element e : structure.getIterator()) {
    e.accept(visitor);
}
  • 访问者池:缓存常用访问者实例(如导出器)
  • 动态访问者:通过反射实现动态visit()方法绑定

模式协作

  • 与组合模式:遍历树形结构(如accept()递归调用子节点)
  • 与解释器模式:AST遍历执行语义动作
  • 与装饰器模式:为访问者添加额外功能(如日志记录)

延伸思考

  • 函数式替代:Java的Pattern Matching、PHP的match可简化类型判断
  • 多语言支持:静态语言(Java)更易实现,动态语言(PHP)需显式类型声明
  • 性能权衡:虚方法调用 vs instanceof性能对比

总结

访问者模式是操作与结构的解耦器,通过双重分发机制实现“操作自由插拔”。其核心价值在于:分离变与不变(数据结构稳定,操作可变)和集中关注点(操作逻辑聚合于访问者)。在编译器、复杂UI框架等场景中,它能优雅应对频繁变动的操作需求,成为架构灵活性的关键支柱。