访问者模式
引言
在软件设计中,当需要对复杂对象结构(如文档树、抽象语法树)添加新操作时,直接修改元素类会导致代码臃肿和开闭原则破坏。访问者模式通过双重分发机制(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 = $50PHP 实现
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框架等场景中,它能优雅应对频繁变动的操作需求,成为架构灵活性的关键支柱。