系列文章目录
第一章 解锁单例模式:Java世界的唯一实例之道
第二章 解锁工厂模式:工厂模式探秘
第三章 解锁代理模式:代理模式的多面解析与实战
第四章 解锁装饰器模式:代码增强的魔法宝典
第五章 解锁建造者模式:Java 编程中的对象构建秘籍
第六章 解锁原型模式:Java 中的高效对象创建之道
第七章 解锁适配器模式:代码重构与架构优化的魔法钥匙
第八章 解锁桥接模式:Java架构中的解耦神器
第九章 解锁组合模式:Java 代码中的树形结构奥秘
第十章 解锁享元模式:内存优化与性能提升的关键密码
第十一章 解锁外观模式:Java 编程中的优雅架构之道
第十二章 解锁观察者模式:Java编程中的高效事件管理之道
第十三章 解锁策略模式:Java 实战与应用全景解析
第十四章 解锁状态模式:Java 编程中的行为魔法
第十五章 解锁模板方法模式:Java 实战与应用探秘
第十六章 解锁命令模式:Java 编程中的解耦神器
第十七章 解锁迭代器模式:Java 编程的遍历神器
第十八章 解锁责任链模式:Java 实战与应用探秘
第十九章 解锁中介者模式:代码世界的“社交达人”
第二十章 解锁备忘录模式:代码世界的时光机
第二十一章 解锁访问者模式:Java编程的灵活之道
第二十二章 解锁Java解释器模式:概念、应用与实战
文章目录
- 引言:探索访问者模式的奥秘
- 一、访问者模式的基本概念
- (一)定义与核心思想
- (二)主要角色解析
- 二、访问者模式的工作原理
- (一)双分派机制详解
- (二)访问者模式的执行流程
- 三、Java 实现访问者模式的步骤与代码示例
- (一)定义抽象访问者和具体访问者
- (二)定义抽象元素和具体元素
- (三)构建对象结构
- (四)客户端调用与测试
- 四、访问者模式的使用场景
- (一)对象结构稳定但操作多变的场景
- (二)需要对对象进行多种不相关操作的场景
- (三)数据结构与操作分离的场景
- 五、访问者模式的优缺点分析
- (一)优点
- (二)缺点
- 六、总结与展望
- (一)访问者模式的核心要点回顾
- (二)对未来学习和应用的建议
引言:探索访问者模式的奥秘
在软件开发的广袤世界里,设计模式宛如璀璨星辰,照亮着开发者前行的道路。它们是前人智慧的结晶,是解决特定问题的通用方案。而访问者模式,作为其中独特的一员,以其别具一格的设计理念和强大的功能,在众多设计模式中独树一帜。
想象一下,你置身于一家热闹非凡的超市,购物车中装满了琳琅满目的商品,有新鲜诱人的水果、美味可口的零食、实用的生活用品等等。当你推着购物车来到收银台时,收银员需要根据不同商品的价格、折扣以及会员等级来计算最终的总价。这看似平常的购物场景,却蕴含着访问者模式的核心思想。
不同类型的商品就如同访问者模式中的具体元素,它们各自有着独特的属性和行为。而收银员则像是访问者,能够对不同的商品进行统一的访问和处理。同时,超市的购物车可以看作是对象结构,它容纳了各种商品,为访问者提供了遍历和操作这些元素的场所。
在这个场景中,我们可以发现,商品的种类可能会不断增加,新的商品可能会加入超市的货架;同时,对于商品的处理方式也可能会发生变化,比如不同的促销活动、会员制度的调整等。如果我们采用传统的编程方式,可能需要频繁地修改商品类和处理商品的代码,这无疑会增加代码的复杂度和维护成本。而访问者模式的出现,为我们提供了一种优雅的解决方案。它能够将数据结构和作用于数据结构上的操作解耦,使得我们可以在不改变商品类的前提下,灵活地定义和添加新的操作。
通过这个超市购物的例子,我们对访问者模式有了一个初步的感性认识。接下来,让我们深入探索访问者模式的概念、原理、结构、实现方式、应用场景以及优缺点,揭开它神秘的面纱,领略它在软件开发中的独特魅力和强大威力。
一、访问者模式的基本概念
(一)定义与核心思想
访问者模式(Visitor Pattern)是一种将数据操作和数据结构分离的设计模式 ,它允许在不修改现有对象结构的情况下,定义作用于这些对象的新操作。其核心思想在于把对数据结构中元素的操作封装成独立的访问者类,使得操作的变化不会影响到数据结构本身,反之亦然。这就好比一个博物馆,馆内的展品(数据结构)是固定的,但不同的导游(访问者)可以根据自己的讲解风格和重点(操作)来向游客介绍这些展品,而不会改变展品本身。
在软件开发中,当我们面对一个复杂的数据结构,并且需要对其元素执行多种不同且可能变化的操作时,访问者模式就展现出了它的强大优势。例如,在一个图形绘制系统中,存在各种形状的图形对象,如圆形、矩形、三角形等(构成数据结构)。如果我们将绘制、缩放、旋转等操作直接定义在图形对象类中,随着操作的增加和变化,图形对象类会变得臃肿不堪,代码的维护和扩展也会变得异常困难。而使用访问者模式,我们可以将这些操作分别封装在不同的访问者类中,图形对象类只需要负责提供接受访问者的方法,这样就实现了数据结构和操作的解耦,使得系统更加灵活和可维护。
(二)主要角色解析
访问者模式包含以下几个主要角色:
- 抽象访问者(Visitor):
抽象访问者定义了一系列访问具体元素的接口方法,这些方法的参数通常是具体元素类型。它为访问具体元素提供了统一的抽象规范,是访问者模式的核心接口之一。通过这个接口,具体访问者可以定义对不同类型元素的操作逻辑。例如:
public interface Visitor {
void visit(ConcreteElementA elementA);
void visit(ConcreteElementB elementB);
}
在这个示例中,Visitor接口定义了visit(ConcreteElementA elementA)和visit(ConcreteElementB elementB)两个方法,分别用于访问ConcreteElementA和ConcreteElementB类型的具体元素。这就像是一个通用的操作模板,具体的操作实现由具体访问者来完成。每个方法对应一种具体元素类型,确保了对不同元素的操作可以通过这个接口进行统一的定义和调用。
- 具体访问者(ConcreteVisitor):
具体访问者实现了抽象访问者接口中定义的方法,针对不同的具体元素执行特定的操作。它是访问者模式中真正实现操作逻辑的部分。例如,在一个电商系统中,我们可以定义一个计算商品总价的访问者:
public class TotalPriceVisitor implements Visitor {
private double totalPrice = 0;
@Override
public void visit(Book book) {
totalPrice += book.getPrice();
}
@Override
public void visit(ElectronicProduct electronicProduct) {
totalPrice += electronicProduct.getPrice();
}
public double getTotalPrice() {
return totalPrice;
}
}
在这个例子中,TotalPriceVisitor实现了Visitor接口,并重写了visit(Book book)和visit(ElectronicProduct electronicProduct)方法。在visit方法中,根据不同的商品类型(书籍和电子产品),将它们的价格累加到totalPrice变量中,从而实现了计算商品总价的功能。通过这种方式,具体访问者可以根据业务需求对不同类型的元素进行个性化的操作,而不会影响到元素本身的结构和其他操作。
- 抽象元素(Element):
抽象元素定义了一个接受访问者的方法accept(Visitor visitor),该方法用于接收访问者对象,使得访问者能够访问该元素。它是所有具体元素的抽象基类或接口,为具体元素提供了统一的接受访问者的规范。例如:
public interface Product {
void accept(Visitor visitor);
}
在这个商品接口示例中,Product接口定义了accept方法,任何实现该接口的具体商品类都必须实现这个方法,以接受访问者的访问。这个方法就像是一个入口,允许访问者进入元素内部进行操作,是访问者模式中元素与访问者交互的关键桥梁。
- 具体元素(ConcreteElement):
具体元素实现了抽象元素中定义的接受访问者的方法,在实现中通常会调用访问者的相应访问方法,将自身作为参数传递给访问者,从而完成对自身的访问操作。例如:
public class Book implements Product {
private String title;
private String author;
private double price;
public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public double getPrice() {
return price;
}
}
public class ElectronicProduct implements Product {
private String model;
private String brand;
private double price;
public ElectronicProduct(String model, String brand, double price) {
this.model = model;
this.brand = brand;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public double getPrice() {
return price;
}
}
在上述代码中,Book和ElectronicProduct类分别实现了Product接口的accept方法。在accept方法中,调用了访问者的visit方法,并将自身(this)作为参数传递进去。这样,当访问者访问这些具体元素时,就可以执行在具体访问者中定义的针对该元素的操作。通过这种方式,具体元素将操作的实现委托给了访问者,实现了数据结构和操作的分离。
- 对象结构(ObjectStructure):
对象结构是一个包含元素集合的容器,它负责管理元素对象,并提供遍历这些元素的方法,以便访问者能够对集合中的所有元素进行访问。例如:
import java.util.ArrayList;
import java.util.List;
public class ProductList {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public void removeProduct(Product product) {
products.remove(product);
}
public void accept(Visitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
}
}
在这个商品集合类的例子中,ProductList类维护了一个Product类型的列表products,并提供了addProduct和removeProduct方法来管理商品元素。accept方法则遍历列表中的所有商品元素,并调用每个元素的accept方法,让访问者对其进行访问。通过对象结构,访问者可以方便地对整个元素集合进行统一的操作,而无需关心具体元素的存储和管理细节。
二、访问者模式的工作原理
(一)双分派机制详解
在理解访问者模式的工作原理时,双分派机制是一个关键概念。为了更好地理解双分派,我们先来回顾一下静态分派和动态分派。
- 静态分派与动态分派结合代码实例
- 静态分派:静态分派基于静态类型在编译期进行方法选择。方法重载是静态分派的典型应用。例如:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class AnimalProcessor {
public void process(Animal animal) {
System.out.println("Processing animal");
}
public void process(Dog dog) {
System.out.println("Processing dog");
}
public void process(Cat cat) {
System.out.println("Processing cat");
}
}
public class StaticDispatchExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
AnimalProcessor processor = new AnimalProcessor();
processor.process(dog);
processor.process(cat);
}
}
在这个例子中,dog和cat的静态类型都是Animal,尽管它们的实际类型分别是Dog和Cat。在编译期,编译器根据参数的静态类型Animal来选择process(Animal animal)方法,所以输出结果都是 “Processing animal” 。这表明静态分派是根据变量的静态类型来确定调用哪个重载方法的,它发生在编译期。
- 动态分派:动态分派基于对象实际类型在运行期置换方法,方法重写是动态分派的典型表现。例如:
class Animal {
public void makeSound() {
System.out.println("Animal makes sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}
public class DynamicDispatchExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound();
cat.makeSound();
}
}
在这个示例中,dog和cat的静态类型都是Animal,但实际类型分别是Dog和Cat。在运行时,JVM 根据对象的实际类型来决定调用哪个重写后的makeSound方法。所以,dog.makeSound()会输出 “Dog barks”,cat.makeSound()会输出 “Cat meows”。这体现了动态分派是在运行期根据对象的实际类型来确定方法的执行版本 。
- 双分派在访问者模式中的实现
双分派是指在选择方法时,不仅要根据对象的实际类型,还要根据参数的实际类型来确定执行的操作。在访问者模式中,通过两次方法调用实现双分派。
以一个简单的图形绘制系统为例,假设有圆形和矩形两种图形,我们要实现一个访问者来计算它们的面积和周长:
// 抽象元素
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 getRadius() {
return 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 getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
// 抽象访问者
interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
// 具体访问者:计算面积和周长
class AreaAndPerimeterVisitor implements ShapeVisitor {
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
double perimeter = 2 * Math.PI * circle.getRadius();
System.out.println("Circle - Area: " + area + ", Perimeter: " + perimeter);
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight());
System.out.println("Rectangle - Area: " + area + ", Perimeter: " + perimeter);
}
}
// 测试
public class VisitorPatternDemo {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
ShapeVisitor visitor = new AreaAndPerimeterVisitor();
circle.accept(visitor);
rectangle.accept(visitor);
}
}
在这个例子中,首先circle.accept(visitor)和rectangle.accept(visitor)这一步是根据对象的实际类型(Circle和Rectangle)进行的第一次分派,调用了具体元素类中实现的accept方法。在accept方法中,visitor.visit(this)又根据参数this(即具体的Circle或Rectangle对象)的实际类型进行了第二次分派,调用了ShapeVisitor接口中对应的visit方法。这样,通过两次方法调用,根据元素类型和访问者类型决定了最终执行的操作,实现了双分派。
(二)访问者模式的执行流程
下面通过详细的代码跟踪,展示访问者模式从客户端创建对象结构和访问者,到元素接受访问者访问,最终执行具体操作的完整流程。
假设我们有一个电商系统,要计算购物车中不同商品的总价和折扣价。
- 定义抽象元素和具体元素
// 抽象元素:商品
interface Product {
void accept(ProductVisitor visitor);
}
// 具体元素:书籍
class Book implements Product {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
@Override
public void accept(ProductVisitor visitor) {
visitor.visit(this);
}
public double getPrice() {
return price;
}
}
// 具体元素:电子产品
class ElectronicProduct implements Product {
private String model;
private double price;
public ElectronicProduct(String model, double price) {
this.model = model;
this.price = price;
}
@Override
public void accept(ProductVisitor visitor) {
visitor.visit(this);
}
public double getPrice() {
return price;
}
}
- 定义抽象访问者和具体访问者
// 抽象访问者
interface ProductVisitor {
void visit(Book book);
void visit(ElectronicProduct electronicProduct);
}
// 具体访问者:计算总价和折扣价
class PriceCalculatorVisitor implements ProductVisitor {
private double totalPrice = 0;
private double totalDiscountedPrice = 0;
@Override
public void visit(Book book) {
totalPrice += book.getPrice();
// 假设书籍打9折
totalDiscountedPrice += book.getPrice() * 0.9;
}
@Override
public void visit(ElectronicProduct electronicProduct) {
totalPrice += electronicProduct.getPrice();
// 假设电子产品打8折
totalDiscountedPrice += electronicProduct.getPrice() * 0.8;
}
public double getTotalPrice() {
return totalPrice;
}
public double getTotalDiscountedPrice() {
return totalDiscountedPrice;
}
}
- 定义对象结构
import java.util.ArrayList;
import java.util.List;
// 对象结构:购物车
class ShoppingCart {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public void accept(ProductVisitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
}
}
- 客户端代码
public class EcommerceSystem {
public static void main(String[] args) {
// 创建对象结构:购物车
ShoppingCart cart = new ShoppingCart();
// 添加商品到购物车
cart.addProduct(new Book("Design Patterns", 50));
cart.addProduct(new ElectronicProduct("Laptop", 1000));
// 创建访问者
PriceCalculatorVisitor visitor = new PriceCalculatorVisitor();
// 购物车接受访问者访问
cart.accept(visitor);
// 输出结果
System.out.println("Total Price: " + visitor.getTotalPrice());
System.out.println("Total Discounted Price: " + visitor.getTotalDiscountedPrice());
}
}
在上述代码中,执行流程如下:
-
客户端创建对象结构和访问者:在main方法中,创建了ShoppingCart对象cart作为对象结构,并添加了Book和ElectronicProduct两种商品。同时,创建了PriceCalculatorVisitor对象visitor作为具体访问者。
-
元素接受访问者访问:调用cart.accept(visitor),ShoppingCart的accept方法遍历其内部的商品列表,依次调用每个商品的accept方法。例如,对于Book对象,调用book.accept(visitor),这是根据对象的实际类型(Book)进行的第一次分派,进入Book类的accept方法。
-
执行具体操作:在Book类的accept方法中,调用visitor.visit(this),这里的this指的是当前的Book对象。这是根据参数的实际类型(Book)进行的第二次分派,调用PriceCalculatorVisitor类中针对Book的visit方法,在该方法中进行书籍价格的计算和折扣处理。同理,对于ElectronicProduct对象也进行类似的操作。
-
获取结果:最后,通过访问者的getTotalPrice和getTotalDiscountedPrice方法获取计算结果并输出。
通过这样的执行流程,访问者模式实现了对不同类型元素的统一访问和处理,将数据结构和操作解耦,使得系统在添加新的操作或元素时更加灵活和可维护。
三、Java 实现访问者模式的步骤与代码示例
(一)定义抽象访问者和具体访问者
以电子商务系统为例,我们首先定义抽象访问者接口,它声明了访问不同类型商品的方法。然后创建具体访问者类,实现抽象访问者接口中定义的方法,完成对商品的具体操作。
// 抽象访问者接口
interface ProductVisitor {
void visit(Book book);
void visit(ElectronicProduct electronicProduct);
}
// 计算价格的具体访问者
class CalculatePriceVisitor implements ProductVisitor {
private double totalPrice = 0;
@Override
public void visit(Book book) {
totalPrice += book.getPrice();
}
@Override
public void visit(ElectronicProduct electronicProduct) {
totalPrice += electronicProduct.getPrice();
}
public double getTotalPrice() {
return totalPrice;
}
}
// 打印商品信息的具体访问者
class PrintProductInfoVisitor implements ProductVisitor {
@Override
public void visit(Book book) {
System.out.println("Book: Title - " + book.getTitle() + ", Author - " + book.getAuthor() + ", Price - " + book.getPrice());
}
@Override
public void visit(ElectronicProduct electronicProduct) {
System.out.println("Electronic Product: Model - " + electronicProduct.getModel() + ", Brand - " + electronicProduct.getBrand() + ", Price - " + electronicProduct.getPrice());
}
}
(二)定义抽象元素和具体元素
接下来,我们定义抽象元素接口,它声明了接受访问者的方法。具体元素类实现抽象元素接口,提供接受访问者的具体实现,并包含自身的业务逻辑。
// 抽象元素接口
interface Product {
void accept(ProductVisitor visitor);
}
// 书籍类,具体元素
class Book implements Product {
private String title;
private String author;
private double price;
public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
@Override
public void accept(ProductVisitor visitor) {
visitor.visit(this);
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public double getPrice() {
return price;
}
}
// 电子产品类,具体元素
class ElectronicProduct implements Product {
private String model;
private String brand;
private double price;
public ElectronicProduct(String model, String brand, double price) {
this.model = model;
this.brand = brand;
this.price = price;
}
@Override
public void accept(ProductVisitor visitor) {
visitor.visit(this);
}
public String getModel() {
return model;
}
public String getBrand() {
return brand;
}
public double getPrice() {
return price;
}
}
(三)构建对象结构
我们创建一个对象结构类,用于管理商品集合。它提供添加商品和接受访问者访问的方法,使得访问者能够遍历并操作集合中的所有商品。
import java.util.ArrayList;
import java.util.List;
// 对象结构类
class ProductList {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public void accept(ProductVisitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
}
}
(四)客户端调用与测试
在客户端代码中,我们创建对象结构和访问者,将商品添加到对象结构中,然后让对象结构接受访问者的访问,从而执行具体的操作并输出结果。
public class EcommerceSystem {
public static void main(String[] args) {
// 创建对象结构
ProductList productList = new ProductList();
// 添加商品到对象结构
productList.addProduct(new Book("Effective Java", "Joshua Bloch", 50));
productList.addProduct(new ElectronicProduct("iPhone 14", "Apple", 999));
// 创建计算价格的访问者
CalculatePriceVisitor priceVisitor = new CalculatePriceVisitor();
productList.accept(priceVisitor);
System.out.println("Total Price: " + priceVisitor.getTotalPrice());
// 创建打印商品信息的访问者
PrintProductInfoVisitor infoVisitor = new PrintProductInfoVisitor();
productList.accept(infoVisitor);
}
}
上述代码中,EcommerceSystem类作为客户端,首先创建了ProductList对象productList,并向其中添加了Book和ElectronicProduct两种商品。然后分别创建了CalculatePriceVisitor和PrintProductInfoVisitor两种访问者,通过productList.accept(visitor)方法,让访问者对商品集合中的商品进行访问和操作。CalculatePriceVisitor计算出商品的总价并输出,PrintProductInfoVisitor打印出每个商品的详细信息。通过这样的方式,展示了访问者模式在实际应用中的运行效果,实现了对不同类型商品的统一访问和多样化操作。
四、访问者模式的使用场景
(一)对象结构稳定但操作多变的场景
在图形绘制系统中,常常存在各种不同类型的图形,如圆形、矩形、三角形等。这些图形构成了一个相对稳定的对象结构。然而,随着业务需求的不断变化,可能需要对这些图形执行多种不同且经常变化的操作,如计算图形的面积、周长,绘制图形,对图形进行缩放、旋转等操作。
如果将这些操作直接定义在图形类中,随着操作种类的增加,图形类会变得越来越臃肿,代码的维护和扩展也会变得异常困难。而且,每增加一种新的操作,都需要修改所有图形类的代码,这显然违背了软件设计中的开闭原则。
使用访问者模式,我们可以将这些操作封装在不同的访问者类中。图形类只需要提供接受访问者的方法,而具体的操作实现则由访问者类来完成。这样,当需要添加新的操作时,只需要创建一个新的访问者类,而不需要修改图形类的代码。
以计算图形面积为例,我们可以定义如下代码:
// 抽象元素:图形
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 getRadius() {
return 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 getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
// 抽象访问者:图形访问者
interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
// 具体访问者:计算面积的访问者
class AreaVisitor implements ShapeVisitor {
@Override
public void visit(Circle circle) {
double area = Math.PI * circle.getRadius() * circle.getRadius();
System.out.println("Circle - Area: " + area);
}
@Override
public void visit(Rectangle rectangle) {
double area = rectangle.getWidth() * rectangle.getHeight();
System.out.println("Rectangle - Area: " + area);
}
}
// 测试代码
public class GraphicsSystem {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
ShapeVisitor areaVisitor = new AreaVisitor();
circle.accept(areaVisitor);
rectangle.accept(areaVisitor);
}
}
在上述代码中,Shape是抽象元素接口,Circle和Rectangle是具体元素类,它们构成了稳定的对象结构。ShapeVisitor是抽象访问者接口,AreaVisitor是具体访问者类,用于计算图形的面积。通过访问者模式,我们可以方便地添加新的操作,如计算周长、绘制图形等,而无需修改图形类的代码。
(二)需要对对象进行多种不相关操作的场景
在员工绩效评定系统中,员工是系统中的主要对象,包括工程师、经理等不同类型的员工。CEO 和 CTO 作为系统的不同使用者,对员工有着不同的关注点和操作需求。
CEO 更关注员工的整体绩效指标(KPI)以及经理的新产品数量,以此来评估员工对公司整体业绩的贡献;而 CTO 则更关注工程师的代码量、技术能力以及经理对项目的技术管理能力,从技术角度来评估员工的工作表现。
如果将 CEO 和 CTO 对员工的评估操作直接放在员工类中,会导致员工类的职责不单一,代码混乱,而且不同的操作逻辑相互交织,难以维护和扩展。例如,当公司引入新的评估指标或改变评估方式时,需要修改员工类的代码,这可能会影响到其他与员工类相关的功能。
使用访问者模式,我们可以将 CEO 和 CTO 的评估操作分别封装在不同的访问者类中。员工类只需要提供接受访问者的方法,这样可以有效地避免操作污染对象类,使得系统的结构更加清晰,代码的维护和扩展更加容易。
以下是使用访问者模式实现员工绩效评定系统的示例代码:
// 抽象员工类
abstract class Staff {
protected String name;
protected int kpi;
public Staff(String name) {
this.name = name;
this.kpi = (int) (Math.random() * 10);
}
public abstract void accept(Visitor visitor);
public String getName() {
return name;
}
public int getKpi() {
return kpi;
}
}
// 工程师类
class Engineer extends Staff {
public Engineer(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public int getCodeLines() {
return (int) (Math.random() * 10000);
}
}
// 经理类
class Manager extends Staff {
public Manager(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public int getProducts() {
return (int) (Math.random() * 10);
}
}
// 抽象访问者接口
interface Visitor {
void visit(Engineer engineer);
void visit(Manager manager);
}
// CEO访问者类
class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("CEO关注:【程序员[" + engineer.getName() + "]的KPI:" + engineer.getKpi() + "】");
}
@Override
public void visit(Manager manager) {
System.out.println("CEO关注:【经理[" + manager.getName() + "]的KPI:" + manager.getKpi() + ",新产品数量:" + manager.getProducts() + "】");
}
}
// CTO访问者类
class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("CTO关注:【程序员[" + engineer.getName() + "]的代码数量:" + engineer.getCodeLines() + "】");
}
@Override
public void visit(Manager manager) {
System.out.println("CTO关注:【产品经理[" + manager.getName() + "]的产品数量:" + manager.getProducts() + "】");
}
}
// 员工列表类,作为对象结构
class StaffList {
private List<Staff> staffList = new ArrayList<>();
public void addStaff(Staff staff) {
staffList.add(staff);
}
public void accept(Visitor visitor) {
for (Staff staff : staffList) {
staff.accept(visitor);
}
}
}
// 测试代码
public class PerformanceEvaluationSystem {
public static void main(String[] args) {
StaffList staffList = new StaffList();
staffList.addStaff(new Engineer("张三"));
staffList.addStaff(new Manager("李四"));
Visitor ceoVisitor = new CEOVisitor();
Visitor ctoVisitor = new CTOVisitor();
staffList.accept(ceoVisitor);
System.out.println("--------------------------");
staffList.accept(ctoVisitor);
}
}
在上述代码中,Staff是抽象员工类,Engineer和Manager是具体员工类,构成了对象结构。Visitor是抽象访问者接口,CEOVisitor和CTOVisitor是具体访问者类,分别实现了 CEO 和 CTO 对员工的评估操作。通过访问者模式,不同的访问者可以对员工进行不同的操作,而不会污染员工类,使得系统的扩展性和维护性得到了极大的提升。
(三)数据结构与操作分离的场景
在编译器的设计中,表达式树是一个非常重要的数据结构,它用于表示程序中的各种表达式,如算术表达式、逻辑表达式等。表达式树的节点包括操作数节点(如变量、常量)和操作符节点(如加、减、乘、除等)。
在编译器的处理过程中,需要对表达式树进行多种操作,其中类型检查是一个关键的环节。类型检查的目的是确保表达式中的操作数和操作符的类型匹配,以保证程序的正确性和安全性。例如,在一个算术表达式中,不能将字符串类型的操作数与数字类型的操作数进行加法运算。
如果将类型检查的操作直接放在表达式树的节点类中,会导致节点类的职责过重,不仅要处理表达式树的结构和数据,还要实现类型检查的逻辑。而且,随着编译器功能的扩展,可能需要对表达式树进行更多不同的操作,如代码生成、优化等,这会使节点类变得越来越复杂,难以维护和扩展。
使用访问者模式,我们可以将类型检查的操作封装在一个独立的访问者类中。表达式树的节点类只需要提供接受访问者的方法,将操作的实现委托给访问者类。这样,表达式树的数据结构和操作就实现了分离,使得代码的结构更加清晰,易于维护和扩展。
以下是一个简单的示例,展示如何使用访问者模式实现表达式树的类型检查:
// 抽象表达式节点类
abstract class Expression {
public abstract void accept(TypeCheckerVisitor visitor);
}
// 操作数节点类
class Operand extends Expression {
private Object value;
private String type;
public Operand(Object value, String type) {
this.value = value;
this.type = type;
}
@Override
public void accept(TypeCheckerVisitor visitor) {
visitor.visit(this);
}
public Object getValue() {
return value;
}
public String getType() {
return type;
}
}
// 操作符节点类
class Operator extends Expression {
private String operator;
private Expression left;
private Expression right;
public Operator(String operator, Expression left, Expression right) {
this.operator = operator;
this.left = left;
this.right = right;
}
@Override
public void accept(TypeCheckerVisitor visitor) {
visitor.visit(this);
}
public String getOperator() {
return operator;
}
public Expression getLeft() {
return left;
}
public Expression getRight() {
return right;
}
}
// 抽象访问者接口:类型检查访问者
interface TypeCheckerVisitor {
void visit(Operand operand);
void visit(Operator operator);
}
// 具体访问者:类型检查实现
class TypeChecker implements TypeCheckerVisitor {
@Override
public void visit(Operand operand) {
// 操作数类型检查,这里简单打印操作数类型
System.out.println("Operand type: " + operand.getType());
}
@Override
public void visit(Operator operator) {
operator.getLeft().accept(this);
operator.getRight().accept(this);
// 操作符类型检查逻辑,这里简单示例,判断左右子表达式类型是否匹配
String leftType = ((Operand) operator.getLeft()).getType();
String rightType = ((Operand) operator.getRight()).getType();
if (!leftType.equals(rightType)) {
System.out.println("Type mismatch for operator: " + operator.getOperator());
} else {
System.out.println("Type check passed for operator: " + operator.getOperator());
}
}
}
// 测试代码
public class CompilerExample {
public static void main(String[] args) {
Expression operand1 = new Operand(5, "int");
Expression operand2 = new Operand(3, "int");
Expression operator = new Operator("+", operand1, operand2);
TypeCheckerVisitor typeChecker = new TypeChecker();
operator.accept(typeChecker);
}
}
在上述代码中,Expression是抽象表达式节点类,Operand和Operator是具体的表达式节点类,构成了表达式树的数据结构。TypeCheckerVisitor是抽象访问者接口,TypeChecker是具体访问者类,实现了类型检查的操作。通过访问者模式,成功地将表达式树的数据结构和类型检查操作分离,使得编译器的代码更加模块化和可维护。当需要添加新的操作时,只需要创建新的访问者类,而无需修改表达式树的节点类,体现了访问者模式在数据结构与操作分离场景中的强大优势。
五、访问者模式的优缺点分析
(一)优点
-
扩展性良好:在访问者模式中,增加新操作只需添加新的访问者类,无需修改元素类,符合开闭原则。以电商系统为例,假设系统中有商品类Product,具体商品如Book和ElectronicProduct。如果我们需要添加一个新的操作,比如计算商品的折扣后价格,按照传统方式,可能需要在Product及其子类中添加相应的方法,这会涉及到修改多个类的代码,且容易出错。而使用访问者模式,我们只需创建一个新的访问者类DiscountPriceVisitor,实现对不同商品计算折扣后价格的方法。这样,当有新的操作需求时,只需要添加新的访问者类,不会影响到已有的元素类和其他访问者类,大大提高了系统的扩展性。
-
复用性高:多个不同的操作都可以使用相同的对象结构,提高了代码复用性。例如在图形绘制系统中,对象结构包含圆形、矩形等图形元素。我们可以定义多个不同的访问者,如AreaVisitor用于计算图形面积,PerimeterVisitor用于计算图形周长,DrawVisitor用于绘制图形。这些不同的访问者都可以使用相同的图形对象结构,避免了重复构建数据结构。当系统中需要进行新的操作时,也可以基于已有的对象结构创建新的访问者,而不需要重新构建整个数据结构,从而提高了代码的复用性。
-
灵活性强:访问者模式使操作集合自由演化,不影响系统的数据结构,提高了系统的灵活性。在员工绩效评定系统中,员工对象结构相对稳定,但随着公司业务的发展和管理需求的变化,可能会出现新的绩效评估方式,如引入 360 度评估、关键事件评估等。使用访问者模式,我们可以轻松地添加新的访问者类来实现这些新的评估方式,而无需修改员工类的结构和代码。这使得系统能够快速适应业务的变化,具有更强的灵活性。例如,当公司决定引入 360 度评估时,我们只需创建一个新的访问者类ThreeSixtyDegreeEvaluationVisitor,实现对员工进行 360 度评估的逻辑,而不会影响到原有的员工数据结构和其他评估方式。
(二)缺点
-
增加新元素类困难:增加新元素类时需要在所有具体访问者类中添加相应操作,违背开闭原则。以图形系统为例,假设现有图形系统已经有圆形Circle和矩形Rectangle,并定义了多个访问者类,如AreaVisitor、PerimeterVisitor等。当需要添加一个新的图形类型,如三角形Triangle时,不仅要创建Triangle类,还需要在所有的具体访问者类中添加对Triangle的访问方法。这意味着需要修改大量已有的代码,违背了开闭原则,增加了系统的维护成本和出错的风险。如果有多个具体访问者类,这个过程会变得更加繁琐和复杂,而且容易遗漏对某些访问者类的修改。
-
破坏封装性:具体元素需向访问者公布细节,破坏了对象的封装性。在访问者模式中,具体元素需要实现接受访问者的方法,并在方法中调用访问者的相应操作。这就意味着具体元素需要将自身的一些内部状态或方法暴露给访问者,从而破坏了对象的封装性。例如,在一个财务系统中,账本类AccountBook包含收入和支出等详细信息,当使用访问者模式进行账目分析时,账本类需要将这些内部信息暴露给访问者,如AccountBookViewer。这可能会导致账本类的内部细节被不当访问或修改,影响代码的维护和安全性,也增加了对象之间的耦合度。
-
复杂性增加:在小型系统或简单结构中使用访问者模式可能带来复杂性。访问者模式涉及多个角色和类,包括抽象访问者、具体访问者、抽象元素、具体元素和对象结构。对于小型系统或简单的数据结构,引入访问者模式可能会使代码结构变得复杂,增加了代码的理解和维护难度。过多的类和接口会使代码的层次结构变得复杂,开发者需要花费更多的时间和精力来理解和管理这些类之间的关系。例如,在一个简单的学生成绩管理系统中,学生成绩记录本身结构简单,如果使用访问者模式来处理成绩计算、统计等操作,可能会因为引入过多的类和接口,使得系统变得过于复杂,反而不如直接在成绩记录类中实现相关操作来得简洁明了。
六、总结与展望
(一)访问者模式的核心要点回顾
访问者模式作为一种独特的行为型设计模式,其核心在于巧妙地将数据操作与数据结构分离开来。它通过定义抽象访问者接口,为不同类型的具体元素提供统一的访问操作定义;具体访问者实现这些操作,针对每个具体元素执行特定逻辑。抽象元素定义接受访问者的方法,具体元素实现该方法以允许访问者对其进行访问。对象结构则管理元素集合,并提供遍历元素的方式,使访问者能够对整个结构中的元素进行操作。
从优点来看,访问者模式具有出色的扩展性,增加新操作只需添加新的访问者类,无需修改元素类,严格遵循开闭原则,这使得系统在面对不断变化的业务需求时能够轻松应对。同时,它具备高度的复用性,多个不同操作可基于相同对象结构进行,避免了重复开发,提高了代码的利用率。灵活性也是其显著优势,操作集合可自由演化,而不影响系统的数据结构,让系统更加适应复杂多变的业务场景。
然而,访问者模式也并非完美无缺。增加新元素类时较为困难,需要在所有具体访问者类中添加相应操作,这不仅繁琐,还违背了开闭原则,增加了维护成本。此外,它破坏了对象的封装性,具体元素需向访问者公布细节,这可能导致对象内部状态的不当暴露。在小型系统或简单结构中,使用访问者模式可能会引入不必要的复杂性,增加代码的理解和维护难度。
在适用场景方面,当对象结构稳定但操作多变时,访问者模式能够大显身手,如在图形绘制系统中,图形结构相对固定,而对图形的操作(如计算面积、周长,绘制、缩放、旋转等)却经常变化,此时使用访问者模式可有效解耦操作与结构,方便扩展新操作。在需要对对象进行多种不相关操作的场景,如员工绩效评定系统中,CEO 和 CTO 对员工有不同的评估操作,使用访问者模式可将这些操作分别封装,避免操作污染员工类。同时,在数据结构与操作分离的场景,如编译器中对表达式树的操作,访问者模式能使操作与数据结构各自独立演化,提高系统的可维护性和扩展性。
(二)对未来学习和应用的建议
对于未来的学习和应用,建议读者在深入理解访问者模式的基础上,积极在实际项目中尝试运用。在面对复杂业务需求时,仔细分析是否适合使用访问者模式,充分发挥其优势。同时,要善于将访问者模式与其他设计模式相结合,如在组合模式中使用访问者模式,以解决更复杂的问题,提升系统的设计质量。
在学习过程中,多参考优秀的开源项目,观察其中访问者模式的应用方式和技巧,不断积累经验。通过实际项目的锻炼,加深对访问者模式的理解和掌握程度,提高自己的设计和编程能力。随着技术的不断发展和业务需求的日益复杂,持续关注设计模式的发展动态,探索访问者模式在新场景下的应用可能性,为软件开发提供更高效、灵活的解决方