1. 概念
- 访问者模式(Visitor Pattern)是一种行为型设计模式。是一种将数据操作与数据结构分离的设计模式,其主要目的是将数据结构与数据操作解耦。
2. 原理结构图
图1
- Visitor(访问者):接口或抽象类,它定义了对每个元素访问的行为。访问者接口中的方法数量理论上与元素的数量是一样的,因为它需要为每种类型的元素提供一个访问方法。这个角色是访问者模式的核心,它使得可以在不修改元素类的情况下增加新的操作。
- ConcreteVisitor(具体访问者):实现Visitor接口的具体类,它实现了对每种元素的具体操作。
- Element(元素):接口或抽象类,定义了一个接受访问者的接口,通常包含一个accept方法,该方法接受一个访问者对象作为参数。
- ConcreteElement(具体元素):实现Element接口的具体类,它实现了接受访问者的方法,通常在accept方法中调用访问者的访问方法。
- ObjectStructure(对象结构):这是包含各种元素的容器,如列表、集合或映射等。它需要提供迭代访问元素的方法,以便访问者可以访问这些元素。
3. 代码示例
3.1 示例1–图形结构
- 有一个复杂的图形结构,其中包含不同类型的节点,如圆形、矩形和三角形。希望对这些节点执行不同的操作,如计算面积、周长和打印节点信息。
// 元素接口
interface Shape { void accept(ShapeVisitor visitor);
} // 具体元素类:圆形
class Circle implements Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); } // 圆形特定的方法 public double getArea() { return Math.PI * radius * radius; } public double getPerimeter() { return 2 * Math.PI * radius; }
} // 具体元素类:矩形
class Rectangle implements Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); } // 矩形特定的方法 public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width + height); }
} // 其他具体元素类(如三角形)可以类似地定义...// 访问者接口
interface ShapeVisitor { void visit(Circle circle); void visit(Rectangle rectangle); // 可以为其他形状添加更多的 visit 方法...
} // 具体访问者类:计算面积
class AreaCalculator implements ShapeVisitor { @Override public void visit(Circle circle) { System.out.println("Circle Area: " + circle.getArea()); } @Override public void visit(Rectangle rectangle) { System.out.println("Rectangle Area: " + rectangle.getArea()); } // 为其他形状实现的 visit 方法...
} // 具体访问者类:计算周长
class PerimeterCalculator implements ShapeVisitor { @Override public void visit(Circle circle) { System.out.println("Circle Perimeter: " + circle.getPerimeter()); } @Override public void visit(Rectangle rectangle) { System.out.println("Rectangle Perimeter: " + rectangle.getPerimeter()); } // 为其他形状实现的 visit 方法...
} // 其他具体访问者类(如打印信息)可以类似地定义...public class ShapeVisitorDemo { public static void main(String[] args) { // 创建形状对象 Shape circle = new Circle(5); Shape rectangle = new Rectangle(4, 6); // 创建访问者对象 ShapeVisitor areaCalculator = new AreaCalculator(); ShapeVisitor perimeterCalculator = new PerimeterCalculator(); // 使用访问者操作形状对象 circle.accept(areaCalculator); // 计算并打印圆形面积 rectangle.accept(areaCalculator); // 计算并打印矩形面积 circle.accept(perimeterCalculator); // 计算并打印圆形周长 rectangle.accept(perimeterCalculator); // 计算并打印矩形周长 // 可以继续添加更多的形状和访问者来执行复杂的操作... }
}
- 输出
Circle Area: 78.53981633974483
Rectangle Area: 24.0
Circle Perimeter: 31.41592653589793
Rectangle Perimeter: 20.0
- 在这个例子中,定义了一个Shape接口和它的两个实现类Circle和Rectangle。每个形状类都有一个accept方法,它接受一个ShapeVisitor类型的参数,并调用其对应的visit方法。为计算面积和周长分别定义了两个ShapeVisitor的实现类AreaCalculator和PerimeterCalculator。
通过accept方法,可以将具体的形状对象与不同的访问者关联起来,执行所需的操作。这样,当需要添加新的操作或新的形状类型时,只需要添加新的访问者类或新的形状实现类,而无需修改现有的代码。这体现了访问者模式的开放封闭原则。
3.2 示例2–电商平台商品处理
- 在电商平台商品处理的复杂场景中,可以使用访问者模式来处理商品的不同操作,例如计算总价、打印商品信息、应用促销折扣等。
// 商品接口
interface Product { void accept(ProductVisitor visitor); double getPrice(); String getName();
} // 具体商品类:电子产品
class ElectronicsProduct implements Product { private String name; private double price; public ElectronicsProduct(String name, double price) { this.name = name; this.price = price; } @Override public void accept(ProductVisitor visitor) { visitor.visit(this); } @Override public double getPrice() { return price; } @Override public String getName() { return name; }
} // 具体商品类:书籍
class BookProduct implements Product { private String name; private double price; public BookProduct(String name, double price) { this.name = name; this.price = price; } @Override public void accept(ProductVisitor visitor) { visitor.visit(this); } @Override public double getPrice() { return price; } @Override public String getName() { return name; }
} // 其他商品类(如服装、食品等)可以类似地定义...// 访问者接口
interface ProductVisitor { void visit(ElectronicsProduct electronics); void visit(BookProduct book); // 可以为其他商品类型添加更多的 visit 方法...
} // 具体访问者类:计算总价
class TotalPriceCalculator implements ProductVisitor { private double total; public double getTotal() { return total; } @Override public void visit(ElectronicsProduct electronics) { total += electronics.getPrice(); } @Override public void visit(BookProduct book) { total += book.getPrice(); } // 为其他商品类型实现的 visit 方法...
} // 具体访问者类:打印商品信息
class ProductInfoPrinter implements ProductVisitor { @Override public void visit(ElectronicsProduct electronics) { System.out.println("Electronics: " + electronics.getName() + ", Price: $" + electronics.getPrice()); } @Override public void visit(BookProduct book) { System.out.println("Book: " + book.getName() + ", Price: $" + book.getPrice()); } // 为其他商品类型实现的 visit 方法...
} // 其他具体访问者类(如应用促销折扣)可以类似地定义...public class ProductVisitorDemo { public static void main(String[] args) { // 创建商品对象 Product electronics = new ElectronicsProduct("Laptop", 999.99); Product book = new BookProduct("Java Programming", 49.95); // 创建访问者对象 ProductVisitor totalPriceCalculator = new TotalPriceCalculator(); ProductVisitor productInfoPrinter = new ProductInfoPrinter(); // 使用访问者操作商品对象 electronics.accept(totalPriceCalculator); book.accept(totalPriceCalculator); System.out.println("Total Price: $" + totalPriceCalculator.getTotal()); electronics.accept(productInfoPrinter); book.accept(productInfoPrinter); // 可以继续添加更多的商品和访问者来执行复杂的操作... }
}
- 将看到如下输出:
Total Price: $1049.94
Electronics: Laptop, Price: $999.99
Book: Java Programming, Price: $49.95
- 在这个例子中,定义了一个Product接口和它的两个实现类ElectronicsProduct和BookProduct。每个商品类都有一个accept方法,它接受一个ProductVisitor类型的参数,并调用其对应的visit方法。为计算总价和打印商品信息分别定义了两个ProductVisitor的实现类TotalPriceCalculator和ProductInfoPrinter。通过accept方法,可以将具体的商品对象与不同的访问者关联起来,执行所需的操作。例如,可以计算商品的总价,也可以打印每个商品的详细信息。如果需要添加新的操作或新的商品
4. 优缺点
- 主要作用
- 访问者模式的主要作用是分离数据结构与数据操作,使得在不改变数据结构的情况下能够定义新的操作。
- 优点
- 高扩展性:能在不修改元素类结构的情况下增加新的操作,提高了系统的可扩展性。
- 操作集中管理:将操作逻辑集中在访问者类中,便于管理和维护。
- 灵活性增强:实现了数据与操作的解耦,使得数据结构更加稳定,操作更加灵活。
- 缺点
- 实现复杂:需要为每个元素类都实现接受和处理访问者的方法,这可能导致代码冗余。
- 增加类数量:每个元素类都需要对应一个访问者类,可能导致系统中类的数量增加。
- 破坏封装性:元素类需要暴露内部状态给访问者,可能违反封装原则。
- 性能开销:元素类与访问者类之间的交互可能增加系统开销。
5. 应用场景
5.1 主要包括以下几个方面
- 数据结构稳定而操作易变:当数据结构相对稳定,但操作容易发生变化时,可以使用访问者模式将操作与数据结构分离,使得操作可以独立地变化和扩展,而不影响数据结构的稳定性。
- 需要为多种对象结构提供统一操作:当需要对多种不同的对象结构执行相同的操作时,可以使用访问者模式定义一个统一的访问者接口,使得不同的对象结构都可以接受相同的访问者进行访问和操作。
- 复杂的对象结构:对于复杂的对象结构,如树形结构或图结构,如果需要对结构中的每个元素都进行某些处理,访问者模式可以很好地遍历整个结构并对每个节点进行统一的操作。
5.2 实际应用
- 博物馆导览系统:在博物馆中,导览员作为访问者,可以访问不同的展品对象(如绘画、雕塑等)。根据游客的兴趣和需求,导览员可以为他们提供不同的讲解和信息,而无需修改展品本身。
- 电商平台商品处理:在电商平台上,商品分类可以被看作是访问者,它能够访问并处理不同类型的商品对象(如服装、数码产品等)。根据业务需求,商品分类可以按照价格、品牌等方式对商品进行排序、筛选等操作。
- 编译器设计:在编译器中,语法树的结构相对固定,但针对不同的语法可能需要执行不同的操作(如语法检查、代码优化等)。通过访问者模式,可以定义不同的访问者类来访问和处理语法树节点,从而实现灵活的编译过程。
- 图形界面编辑:在图形界面编辑软件中,不同的图形对象(如矩形、圆形等)可能具有不同的属性和操作。通过使用访问者模式,可以定义统一的访问者接口来访问和操作这些图形对象,实现灵活的图形编辑功能。
6. JDK中的使用
java.nio.file包中的文件树遍历
- Visitor(访问者)
- FileVisitor接口:这是一个访问者接口,它包含了四个方法,分别对应访问前、访问后、访问失败和访问文件时的动作。
- SimpleFileVisitor类:这是一个简单的访问者实现,用户可以通过继承这个类并重写自己关心的方法来定制自己的访问者行为。
- ConcreteVisitor(具体访问者)
- 用户自定义的FileVisitor实现:用户可以创建一个类,实现FileVisitor接口,提供对文件或目录访问前、访问后以及遇到错误时的具体处理逻辑。
- Element(元素)
- Path接口:代表文件系统中的路径,是文件树结构的基本元素。
- ConcreteElement(具体元素)
- 各种类型的Path对象:如DirectoryNode和FileNode,它们分别表示文件系统中的目录节点和文件节点。
- ObjectStructure(对象结构)
- Files.walkFileTree方法:这个方法接受一个起始Path和一个FileVisitor对象作为参数,它会遍历从给定的Path开始的文件树,使用提供的FileVisitor来处理每个访问到的元素。
通过使用访问者模式,JDK提供了一种灵活的方式来处理文件系统中的文件和目录。用户可以定义自己的FileVisitor实现,而不需要修改Files类的代码,这有助于保持核心API的稳定性。同时,这种模式也使得添加新的操作变得简单,因为只需添加一个新的访问者实现即可,无需改动现有的元素类或对象结构。
7. 注意事项
- 复杂性管理:访问者模式可能会增加系统的复杂性,特别是当元素和访问者类型众多时。确保使用此模式确实带来了所需的灵活性,并仔细考虑维护成本。
- 扩展性考虑:虽然访问者模式易于为元素添加新操作,但为访问者添加新元素类型可能需要修改所有访问者类。在设计时,要预见可能的扩展需求。
- 数据访问封装:访问者模式通常用于封装对数据结构内部元素的访问和操作。确保访问者的设计不会暴露不必要的内部状态,以保持封装性。
- 性能考虑:访问者模式可能涉及多次遍历数据结构,特别是在处理大型数据集时。要评估性能影响,并在必要时进行优化。
- 双重分派:访问者模式依赖于双重分派机制(运行时确定操作),这在某些语言或环境下可能不是最高效的。理解并接受这种机制的性能特性是很重要的。
- 设计模式选择:确保访问者模式是解决问题的最佳方案。有时,其他设计模式可能更适合特定的需求。