访问者模式 (Visitor Pattern)
概述
访问者模式是一种行为型设计模式,它表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
意图
- 表示一个作用于某对象结构中的各元素的操作
- 它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
适用场景
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作"污染"这些对象的类
- 当对象结构被很多应用共享时,用访问者模式让每个应用只包含需要用到的操作
结构
┌─────────────┐ ┌─────────────┐
│ Visitor │<─────────│ Element │
├─────────────┤ ├─────────────┤
│ + visitA() │ │ + accept() │
│ + visitB() │ └─────────────┘
└─────────────┘ ▲
│
┌─────────────┐ ┌─────────────┐
│ObjectStructure│ │ConcreteElement│
├─────────────┤ ├─────────────┤
│ - elements │ │ + accept() │
│ + accept() │ └─────────────┘
└─────────────┘ ▲
│
┌─────────────┐ ┌─────────────┐
│ConcreteVisitor│ │ConcreteElementA│
├─────────────┤ ├─────────────┤
│ + visitA() │ │ + accept() │
│ + visitB() │ │ + operationA() │
└─────────────┘ └─────────────┘
┌─────────────┐
│ConcreteElementB│
├─────────────┤
│ + accept() │
│ + operationB() │
└─────────────┘
参与者
- Visitor:为一个对象结构中ConcreteElement的每一个类声明一个Visit操作
- ConcreteVisitor:实现每个由Visitor声明的操作,每个操作实现算法的一部分
- Element:定义一个Accept操作,它以一个访问者为参数
- ConcreteElement:实现Accept操作,该操作以一个访问者为参数
- ObjectStructure:能枚举它的元素,可以提供一个高层的接口以允许访问者访问它的元素
示例代码
下面是一个完整的访问者模式示例,以购物车为例:
java
import java.util.ArrayList;
import java.util.List;
// Visitor - 访问者接口
public interface ShoppingCartVisitor {
double visit(Book book);
double visit(Fruit fruit);
}
// Element - 元素接口
public interface ItemElement {
double accept(ShoppingCartVisitor visitor);
}
// ConcreteElement - 具体元素类1
public class Book implements ItemElement {
private double price;
private String isbn;
public Book(double price, String isbn) {
this.price = price;
this.isbn = isbn;
}
public double getPrice() {
return price;
}
public String getIsbn() {
return isbn;
}
@Override
public double accept(ShoppingCartVisitor visitor) {
return visitor.visit(this);
}
}
// ConcreteElement - 具体元素类2
public class Fruit implements ItemElement {
private double pricePerKg;
private double weight;
private String name;
public Fruit(double pricePerKg, double weight, String name) {
this.pricePerKg = pricePerKg;
this.weight = weight;
this.name = name;
}
public double getPricePerKg() {
return pricePerKg;
}
public double getWeight() {
return weight;
}
public String getName() {
return name;
}
@Override
public double accept(ShoppingCartVisitor visitor) {
return visitor.visit(this);
}
}
// ConcreteVisitor - 具体访问者类
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
@Override
public double visit(Book book) {
// 书籍超过50元打9折
double cost = book.getPrice();
if (cost > 50) {
cost = cost * 0.9;
}
System.out.println("书籍 ISBN: " + book.getIsbn() + " 价格: " + cost);
return cost;
}
@Override
public double visit(Fruit fruit) {
double cost = fruit.getPricePerKg() * fruit.getWeight();
System.out.println(fruit.getName() + " 价格: " + cost);
return cost;
}
}
// ObjectStructure - 对象结构类
public class ShoppingCart {
private List<ItemElement> items = new ArrayList<>();
public void addItem(ItemElement item) {
items.add(item);
}
public void removeItem(ItemElement item) {
items.remove(item);
}
public double calculateTotalCost(ShoppingCartVisitor visitor) {
double totalCost = 0;
for (ItemElement item : items) {
totalCost += item.accept(visitor);
}
return totalCost;
}
}
// Client - 客户端
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Book(20, "1234"));
cart.addItem(new Book(100, "5678"));
cart.addItem(new Fruit(2.5, 2, "香蕉"));
cart.addItem(new Fruit(5, 1.5, "苹果"));
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
double totalCost = cart.calculateTotalCost(visitor);
System.out.println("总成本: " + totalCost);
}
}
另一个示例 - 文件系统
java
import java.util.ArrayList;
import java.util.List;
// Visitor - 访问者接口
public interface FileSystemVisitor {
void visit(File file);
void visit(Directory directory);
}
// Element - 元素接口
public interface FileSystemElement {
void accept(FileSystemVisitor visitor);
String getName();
}
// ConcreteElement - 具体元素类1
public class File implements FileSystemElement {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visit(this);
}
@Override
public String getName() {
return name;
}
public long getSize() {
return size;
}
}
// ConcreteElement - 具体元素类2
public class Directory implements FileSystemElement {
private String name;
private List<FileSystemElement> elements = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void addElement(FileSystemElement element) {
elements.add(element);
}
public void removeElement(FileSystemElement element) {
elements.remove(element);
}
public List<FileSystemElement> getElements() {
return elements;
}
@Override
public void accept(FileSystemVisitor visitor) {
visitor.visit(this);
// 遍历子元素
for (FileSystemElement element : elements) {
element.accept(visitor);
}
}
@Override
public String getName() {
return name;
}
}
// ConcreteVisitor - 具体访问者类1
public class SizeCalculatorVisitor implements FileSystemVisitor {
private long totalSize = 0;
@Override
public void visit(File file) {
totalSize += file.getSize();
}
@Override
public void visit(Directory directory) {
// 目录本身不占用空间,所以不需要添加大小
}
public long getTotalSize() {
return totalSize;
}
}
// ConcreteVisitor - 具体访问者类2
public class ListPrinterVisitor implements FileSystemVisitor {
private int indent = 0;
@Override
public void visit(File file) {
printIndent();
System.out.println("文件: " + file.getName() + " (" + file.getSize() + " 字节)");
}
@Override
public void visit(Directory directory) {
printIndent();
System.out.println("目录: " + directory.getName());
indent++;
}
private void printIndent() {
for (int i = 0; i < indent; i++) {
System.out.print(" ");
}
}
}
// Client - 客户端
public class Client {
public static void main(String[] args) {
// 创建文件系统结构
Directory root = new Directory("根目录");
Directory documents = new Directory("文档");
documents.addElement(new File("简历.doc", 1024));
documents.addElement(new File("报告.pdf", 2048));
Directory pictures = new Directory("图片");
pictures.addElement(new File("风景.jpg", 3072));
pictures.addElement(new File("人物.png", 4096));
root.addElement(documents);
root.addElement(pictures);
root.addElement(new File("readme.txt", 512));
// 使用访问者计算总大小
SizeCalculatorVisitor sizeCalculator = new SizeCalculatorVisitor();
root.accept(sizeCalculator);
System.out.println("总大小: " + sizeCalculator.getTotalSize() + " 字节");
System.out.println();
// 使用访问者打印文件列表
ListPrinterVisitor listPrinter = new ListPrinterVisitor();
root.accept(listPrinter);
}
}
双分派技术
访问者模式使用了双分派技术,第一次分派是在accept方法中,第二次分派是在visit方法中:
java
// 第一次分派:根据元素的类型调用相应的accept方法
element.accept(visitor);
// 第二次分派:在accept方法中根据访问者的类型调用相应的visit方法
public void accept(FileSystemVisitor visitor) {
visitor.visit(this); // 这里的this是具体的元素类型
}
优缺点
优点
- 访问者模式使得添加新的操作变得容易,只需添加一个新的访问者类即可
- 访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的元素类中
- 访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类
缺点
- 增加新的元素类很困难,需要在每一个访问者类中添加相应的访问操作
- 访问者模式要求元素类要暴露一些内部状态和操作,否则访问者难以实现其功能
- 访问者模式破坏了封装性,元素类必须提供访问其内部状态的公共方法
相关模式
- 组合模式:访问者模式通常与组合模式一起使用,以遍历组合结构中的所有元素
- 迭代器模式:访问者模式和迭代器模式都用于遍历对象结构,但访问者模式关注的是对元素的操作,而迭代器模式关注的是元素的访问
实际应用
- 编译器的语法树遍历
- 文档对象模型(DOM)的遍历
- 数据库查询结果的处理
- 代码生成器
- 复杂对象结构的序列化和反序列化
访问者模式与迭代器模式的区别
- 访问者模式:访问者模式关注的是对元素的操作,可以在不改变元素类的情况下添加新的操作
- 迭代器模式:迭代器模式关注的是元素的访问,提供一种顺序访问聚合对象中各个元素的方法
访问者模式用于对元素进行操作,而迭代器模式用于访问元素。
注意事项
- 访问者模式中的元素类应该提供一个accept方法,以便访问者可以访问它
- 访问者模式中的访问者类应该为每种元素类型提供一个visit方法
- 访问者模式中的元素类应该尽量保持稳定,避免频繁修改
- 访问者模式中的访问者类应该保持轻量级,避免包含过多的状态信息