跳转到内容

Java访问者模式

来自代码酷


访问者模式(Visitor Pattern)是设计模式中一种行为型模式,它允许你将算法与对象结构分离,从而在不修改现有对象结构的情况下定义新的操作。该模式通过将操作逻辑移动到独立的访问者类中,实现了对象结构与操作的解耦。

概述[编辑 | 编辑源代码]

访问者模式的核心思想是:在对象结构中定义一个接受访问者的接口,让访问者能够处理结构中的各个元素。这种模式特别适用于以下场景:

  • 对象结构包含许多不同类型的对象
  • 需要对对象结构中的元素执行多种不相关的操作
  • 不希望频繁修改对象结构的类定义

模式结构[编辑 | 编辑源代码]

访问者模式包含以下主要角色:

  • Visitor(访问者接口) - 声明访问操作
  • ConcreteVisitor(具体访问者) - 实现访问操作
  • Element(元素接口) - 定义接受访问者的方法
  • ConcreteElement(具体元素) - 实现接受访问者的方法
  • ObjectStructure(对象结构) - 维护元素集合,提供遍历接口

classDiagram class Visitor { +visitConcreteElementA(ConcreteElementA) +visitConcreteElementB(ConcreteElementB) } class ConcreteVisitor1 { +visitConcreteElementA(ConcreteElementA) +visitConcreteElementB(ConcreteElementB) } class Element { +accept(Visitor) } class ConcreteElementA { +accept(Visitor) +operationA() } class ConcreteElementB { +accept(Visitor) +operationB() } class ObjectStructure { -elements: List~Element~ +attach(Element) +detach(Element) +accept(Visitor) } Visitor <|-- ConcreteVisitor1 Element <|-- ConcreteElementA Element <|-- ConcreteElementB ConcreteElementA ..> Visitor : «uses» ConcreteElementB ..> Visitor : «uses» ObjectStructure o-- Element

代码示例[编辑 | 编辑源代码]

以下是一个简单的访问者模式实现示例,展示如何计算购物车中不同类型商品的总价:

// 元素接口
interface ItemElement {
    int accept(ShoppingCartVisitor visitor);
}

// 具体元素类
class Book implements ItemElement {
    private int price;
    private String isbn;
    
    public Book(int cost, String isbn) {
        this.price = cost;
        this.isbn = isbn;
    }
    
    public int getPrice() {
        return price;
    }
    
    public String getIsbn() {
        return isbn;
    }
    
    @Override
    public int accept(ShoppingCartVisitor visitor) {
        return visitor.visit(this);
    }
}

class Fruit implements ItemElement {
    private int pricePerKg;
    private int weight;
    private String name;
    
    public Fruit(int priceKg, int wt, String nm) {
        this.pricePerKg = priceKg;
        this.weight = wt;
        this.name = nm;
    }
    
    public int getPricePerKg() {
        return pricePerKg;
    }
    
    public int getWeight() {
        return weight;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public int accept(ShoppingCartVisitor visitor) {
        return visitor.visit(this);
    }
}

// 访问者接口
interface ShoppingCartVisitor {
    int visit(Book book);
    int visit(Fruit fruit);
}

// 具体访问者
class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
    
    @Override
    public int visit(Book book) {
        int cost = book.getPrice();
        // 书籍价格超过50元有5元折扣
        if(cost > 50) {
            cost -= 5;
        }
        System.out.println("Book ISBN::" + book.getIsbn() + " cost = " + cost);
        return cost;
    }
    
    @Override
    public int visit(Fruit fruit) {
        int cost = fruit.getPricePerKg() * fruit.getWeight();
        System.out.println(fruit.getName() + " cost = " + cost);
        return cost;
    }
}

// 对象结构
class ShoppingCart {
    private List<ItemElement> items = new ArrayList<>();
    
    public void addItem(ItemElement item) {
        items.add(item);
    }
    
    public int calculateTotal(ShoppingCartVisitor visitor) {
        int sum = 0;
        for(ItemElement item : items) {
            sum += item.accept(visitor);
        }
        return sum;
    }
}

// 客户端代码
public class VisitorPatternDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Book(100, "1234"));
        cart.addItem(new Book(40, "5678"));
        cart.addItem(new Fruit(10, 2, "Apple"));
        cart.addItem(new Fruit(5, 3, "Banana"));
        
        ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
        int total = cart.calculateTotal(visitor);
        System.out.println("Total Cost = " + total);
    }
}

输出结果:

Book ISBN::1234 cost = 95
Book ISBN::5678 cost = 40
Apple cost = 20
Banana cost = 15
Total Cost = 170

实际应用场景[编辑 | 编辑源代码]

访问者模式在以下场景中特别有用:

1. 编译器设计:在抽象语法树(AST)上执行各种操作(类型检查、代码优化、代码生成等) 2. 文档处理:对文档结构(段落、表格、图片等)执行多种操作(拼写检查、字数统计、格式转换) 3. UI组件处理:对UI组件树执行多种操作(渲染、布局计算、事件处理) 4. 财务系统:对不同类型的财务记录执行多种计算(税收计算、利润分析、审计)

编译器案例[编辑 | 编辑源代码]

在编译器实现中,访问者模式可以优雅地处理抽象语法树上的各种操作:

// 表达式节点接口
interface ExprNode {
    void accept(ExprVisitor visitor);
}

// 具体表达式节点
class NumberNode implements ExprNode {
    private int value;
    
    public NumberNode(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    @Override
    public void accept(ExprVisitor visitor) {
        visitor.visit(this);
    }
}

class AddNode implements ExprNode {
    private ExprNode left, right;
    
    public AddNode(ExprNode l, ExprNode r) {
        left = l;
        right = r;
    }
    
    public ExprNode getLeft() {
        return left;
    }
    
    public ExprNode getRight() {
        return right;
    }
    
    @Override
    public void accept(ExprVisitor visitor) {
        visitor.visit(this);
    }
}

// 访问者接口
interface ExprVisitor {
    void visit(NumberNode node);
    void visit(AddNode node);
}

// 打印访问者
class PrintVisitor implements ExprVisitor {
    @Override
    public void visit(NumberNode node) {
        System.out.print(node.getValue());
    }
    
    @Override
    public void visit(AddNode node) {
        node.getLeft().accept(this);
        System.out.print(" + ");
        node.getRight().accept(this);
    }
}

// 计算访问者
class CalculateVisitor implements ExprVisitor {
    private int result;
    
    public int getResult() {
        return result;
    }
    
    @Override
    public void visit(NumberNode node) {
        result = node.getValue();
    }
    
    @Override
    public void visit(AddNode node) {
        node.getLeft().accept(this);
        int left = result;
        node.getRight().accept(this);
        result = left + result;
    }
}

// 使用示例
public class CompilerExample {
    public static void main(String[] args) {
        // 构建表达式树: 1 + (2 + 3)
        ExprNode expr = new AddNode(
            new NumberNode(1),
            new AddNode(new NumberNode(2), new NumberNode(3))
        );
        
        // 打印表达式
        PrintVisitor printer = new PrintVisitor();
        expr.accept(printer);  // 输出: 1 + 2 + 3
        
        // 计算表达式
        CalculateVisitor calculator = new CalculateVisitor();
        expr.accept(calculator);
        System.out.println(" = " + calculator.getResult());  // 输出: = 6
    }
}

优缺点[编辑 | 编辑源代码]

优点[编辑 | 编辑源代码]

  • 开闭原则:可以引入新的访问者而无需修改现有元素类
  • 单一职责原则:将相关行为集中在一个访问者类中
  • 灵活性:可以在运行时选择不同的访问者来执行不同的操作
  • 可扩展性:容易添加新的操作

缺点[编辑 | 编辑源代码]

  • 破坏封装:访问者通常需要访问元素的内部状态
  • 元素接口变更困难:添加新元素类型需要修改所有访问者
  • 可能违反依赖倒置原则:具体元素类可能依赖于具体访问者

数学表示[编辑 | 编辑源代码]

访问者模式可以形式化表示为:

对于元素集合 E={e1,e2,...,en}和访问者集合 V={v1,v2,...,vm}操作结果 R=i=1nei.accept(vj)对于某个 vjV

与其他模式的关系[编辑 | 编辑源代码]

  • 访问者模式可以看作命令模式的扩展版本,其对象可以操作多个不同类型的接收者
  • 可以结合组合模式使用,对复杂对象结构进行操作
  • 访问者模式经常与迭代器模式一起使用,遍历对象结构并对其元素执行操作

最佳实践[编辑 | 编辑源代码]

1. 当对象结构稳定但需要频繁添加新操作时,优先考虑访问者模式 2. 避免在元素接口中暴露过多内部状态,保持良好封装 3. 考虑使用访问者模式来实现"双重分发"机制 4. 对于简单的对象结构或很少变化的操作集,访问者模式可能过度设计

总结[编辑 | 编辑源代码]

访问者模式提供了一种优雅的方式来分离对象结构和操作逻辑,特别适合处理复杂对象结构上的多种操作。虽然它有一定的复杂性,但在适当的场景下能显著提高代码的可维护性和扩展性。理解并合理应用访问者模式,可以帮助开发者构建更加灵活和可扩展的软件系统。