在软件开发中,经常会遇到需要对一个复杂对象结构中的各个元素进行操作和处理的情况。如果直接在元素类中添加这些操作和处理逻辑,会导致类的职责不清晰、扩展困难等问题。为了解决这些问题,可以使用访问者模式。
访问者模式是一种行为型设计模式,它将数据结构和数据操作分离开来,将操作封装在一个独立的访问者类中。通过让访问者在不修改元素类的前提下,实现对元素的操作,达到了降低耦合度、增加扩展性的目的。
访问者模式包含以下几个主要角色:
元素定义了一个accept方法,该方法接受一个访问者对象作为参数,用于访问和操作元素。
public interface Element {
void accept(Visitor visitor);
}
具体元素实现了元素接口,实现accept方法,将自身作为参数传递给访问者对象,以便访问者可以对其进行操作。
public class ConcreteElement implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
访问者定义了对每个具体元素的访问操作,它为每个具体元素都提供了一个visit方法。
public interface Visitor {
void visit(ConcreteElement element);
}
具体访问者实现了访问者接口,对每个具体元素的visit方法进行具体实现。
public class ConcreteVisitor implements Visitor {
@Override
public void visit(ConcreteElement element) {
// 对具体元素进行操作和处理
}
}
对象结构是一个容器,它存储了多个元素对象,并提供了遍历元素对象的方法。
public class ObjectStructure {
private List<Element> elements = new ArrayList<>();
public void add(Element element) {
elements.add(element);
}
public void remove(Element element) {
elements.remove(element);
}
public void accept(Visitor visitor) {
for (Element element : elements) {
element.accept(visitor);
}
}
}
需求:一个电商系统,里面包含了不同类型的商品,如书籍、服装和电子产品等。需要实现一个功能,即计算购物车中所有商品的总价格,并且在将来可能会添加其他的操作,比如计算商品的折扣价格。
首先,定义一个接口Product表示商品,其中包含了一个accept方法用于接受访问者对象。
public interface Product {
void accept(Visitor visitor);
}
然后,定义了不同类型的具体商品类,它们实现了Product接口,并实现了accept方法。
public class Book implements Product {
private double price;
public Book(double price) {
this.price = price;
}
public double getPrice() {
return price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Clothing implements Product {
private double price;
public Clothing(double price) {
this.price = price;
}
public double getPrice() {
return price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Electronics implements Product {
private double price;
public Electronics(double price) {
this.price = price;
}
public double getPrice() {
return price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
接下来,定义一个访问者接口Visitor,其中包含了不同类型商品的访问方法。
public interface Visitor {
void visit(Book book);
void visit(Clothing clothing);
void visit(Electronics electronics);
}
然后,实现一个具体的访问者类PriceCalculator,用于计算商品的总价格。
public class PriceCalculator implements Visitor {
private double totalPrice;
public double getTotalPrice() {
return totalPrice;
}
@Override
public void visit(Book book) {
totalPrice += book.getPrice();
}
@Override
public void visit(Clothing clothing) {
totalPrice += clothing.getPrice();
}
@Override
public void visit(Electronics electronics) {
totalPrice += electronics.getPrice();
}
}
最后,创建一个购物车对象,并向其中添加不同类型的商品。然后,创建一个PriceCalculator对象,将其作为访问者传递给购物车中的每个商品,从而计算出购物车中所有商品的总价格。
public class ShoppingCart {
private List<Product> products;
public ShoppingCart() {
products = new ArrayList<>();
}
public void addProduct(Product product) {
products.add(product);
}
public void calculateTotalPrice(Visitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
}
}
// 在客户端代码中使用访问者模式
public class Client {
public static void main(String[] args) {
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.addProduct(new Book(50.0));
shoppingCart.addProduct(new Clothing(100.0));
shoppingCart.addProduct(new Electronics(200.0));
PriceCalculator priceCalculator = new PriceCalculator();
shoppingCart.calculateTotalPrice(priceCalculator);
double totalPrice = priceCalculator.getTotalPrice();
System.out.println("Total Price: " + totalPrice);
}
}
通过访问者模式,将商品的操作与商品本身解耦,可以方便地添加新的操作,而不需要修改商品类。在以上例子中,只需要实现一个新的访问者类,即可添加新的操作,比如计算商品的折扣价格。这样,可以灵活地对复杂的对象结构进行操作和扩展。
概念中提到的和我们平时说的数据结构是一回事吗?
在访问者模式中,数据结构是指被访问的对象集合,通常由一个容器类管理。数据结构可以是任何类型的集合,如列表、树、图等。对于每个元素,数据结构提供了接受访问者的方法。我们平时说的数据结构则是一种在计算机内存或其他存储设备上组织和存储数据的方式。它涉及到如何组织和管理数据,以便有效地操作和访问数据。常见的计算机数据结构包括数组、链表、栈、队列、树和图等。虽然访问者模式中的数据结构和计算机数据结构都涉及到处理数据,但它们并不是完全相同的概念。访问者模式中的数据结构是指被访问的对象集合,而计算机数据结构是指在计算机中组织和存储数据的方式。
概念中的对象结构是指用于存储元素对象的容器,它提供了添加、删除元素的功能,并且还提供了一个接受访问者的方法,以便访问者能够遍历其中的元素并进行相应的操作。
除此之外,对象结构还有以下两个重要的功能:
结构管理功能:对象结构可以维护元素对象之间的关系,例如树形结构中的父子关系或链表结构中的前驱后继关系。这样可以方便地对元素对象进行管理和访问。
调度功能:对象结构可以协调元素对象和访问者对象之间的交互过程,例如按照某种顺序依次遍历所有元素对象,并将其传递给访问者对象进行处理。这样可以保证访问者对象得到正确的元素顺序,并且减少了访问者对象之间的竞争和冲突。
虽然类图中没有画出具体元素类和具体访问者类之间的双向依赖关系,但是在代码中是可以看出来的,彼此作为方法的参数:具体访问者作为参数传递给具体元素的accept方法,从而使得具体元素可以调用具体访问者的方法,具体元素类通过调用访问者对象的方法来完成对自身的操作。访问者对象则根据具体元素的类型来执行不同的操作。这种双向依赖关系在代码中可能不明显,因为它们是通过接口之间的关联来实现的,而不是直接依赖具体类。具体元素类实现了接口中的方法,并将自身作为参数传递给访问者的方法。访问者类则通过接口来访问具体元素的方法。
这种双向依赖关系并不是紧耦合的,而是松耦合的。所谓松耦合,是指两个模块之间的相互依赖程度比较低,它们之间的关系可以随时被修改、扩展或替换,而不会对系统的其他部分产生太大的影响。这种设计使得具体元素类和具体访问者类能够独立进行修改、扩展或替换,而不会对系统的其他部分产生太大的影响。
从另一个角度看具体元素类和具体访问者类之间是解耦的,这体现在它们的职责上,具体元素类主要负责自身的数据和行为,而不关心访问者如何处理它们;具体访问者类主要负责对不同类型的具体元素进行访问和处理,而不关心元素的具体实现。这种职责分离也使得访问者模式更加灵活、可扩展和易于维护。
上面提到具体元素类主要负责自身的数据和行为,这里的具体数据指的是元素类所包含的属性或状态信息,具体行为指的是元素类所提供的方法或操作。具体数据可以是元素类内部维护的一些成员变量,用于存储元素对象的状态或属性信息。例如,一个具体元素类可以是一个图形对象,它可能包含表示位置、大小、颜色等属性的数据,具体行为可以是元素类所提供的方法,用于操作或访问元素对象的属性或状态。例如,一个具体元素类可以定义draw()方法,用于绘制图形对象;或者定义getSize()方法,用于获取图形对象的尺寸。
具体元素类的数据和行为与访问者模式的关系是,它们作为元素接口的实现,通过accept()方法将自身传递给访问者对象,以便访问者可以对其进行操作。但具体元素类并不关心访问者如何处理它们的数据和行为,它只负责将自身暴露给访问者,并由访问者实际进行操作。
访问者模式满足了七大原则中的以下几个原则:
单一职责原则(SRP):具体元素类和具体访问者类各自承担了自己的职责。具体元素类负责自身数据和行为,而具体访问者类负责对元素进行访问和处理。
开放封闭原则(OCP):通过接口的方式进行解耦,使得系统中可以新增其他具体元素类和具体访问者类,而无需修改已有的代码。这样可以在不修改现有代码的前提下,对系统进行功能的扩展。
依赖倒置原则(DIP):抽象元素接口和抽象访问者接口定义了具体元素类和具体访问者类之间的依赖关系,具体元素类和具体访问者类依赖于接口而不是具体实现。这样可以降低具体类之间的耦合度,提高系统的可维护性和可扩展性。
接口隔离原则(ISP):抽象元素接口和抽象访问者接口都只包含了必要的方法,避免了接口的臃肿和冗余。具体元素类和具体访问者类只需要实现与其相关的方法,减少了不必要的依赖和耦合。
里氏替换原则(LSP):具体元素类可以通过实现元素接口来替代抽象元素类,具体访问者类可以通过实现访问者接口来替代抽象访问者类。这样可以在不影响系统行为的前提下,进行对象的替换与扩展。
最后我想写一点不太成熟的想法,没有什么依据,仅仅是我自己的理解,设计模式书上关于访问者模式的例子是关于男人和女人在不同状态时有不同的反应,我觉得整体的变化过程是这样的:
开始,直接输出男人和女人在成功、失败、恋爱时的不同反应,显然要增加新的状态,就不符合开闭原则了,面向对象是封装变化点,这里的变化点就是状态和反应作为一个整体的增加,这个时候,封装了一个类Action,成功、失败、恋爱,就是具体的子类。
接着,关于Action(也就是访问者),男人和女人是不一样的,所以里面针对男人和女人就有两个不同的方法,这两个方法就是代码的核心,也就是真正要输出或者说执行的内容。
然后,关于Person,除去自己独有的属性和方法,仅仅只看针对与访问者来说,可以看成是访问者的一个壳,为的就是给访问者包一层,因为方法内部真正调的是访问者的方法。
最后,对象结构,是个容器,用来存放Person,遍历里面的Person元素。
这样浓缩看下来,代码就简单了很多。这就是我的繁化简的过程。当然,关于里面的细节都在上面具体说过了,这里只是去繁就简,让代码更容易理解。
访问者模式通过将数据结构与数据操作分离开来,使得操作可以独立于元素而变化。它能够增加系统的灵活性、扩展性和可维护性,尤其适用于那些数据结构相对稳定,但需要频繁添加新的操作的场景。然而,访问者模式也会增加系统的复杂性,所以在使用时需要根据实际情况进行权衡和选择。