一、访问者模式定义
访问者模式是一种行为型模式,而且是行为型模式中比较复杂的一种模式。
访问者模式(Visitor Pattern)的定义如下:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作(Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.)
**意图:**主要将数据结构与数据操作分离。
**主要解决:**稳定的数据结构和易变的操作耦合问题。
**何时使用:**需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
**如何解决:**在被访问的类里面加一个对外提供接待访问者的接口。
**关键代码:**在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
通用类图如下所示
这里面一共涉及到五种角色:
Visitor(访问者):定义了要访问的元素的操作,可以为每个具体元素提供不同的访问操作。访问者模式中通常会定义多个不同的访问者。
ConcreteVisitor(具体访问者):实现Visitor接口,具体定义了对每个元素的具体访问操作。
Element(元素):定义了一个接受访问者的方法(accept(Visitor visitor)
),该方法将访问者作为参数传递给元素,使访问者能够访问元素的内部状态。
ConcreteElement(具体元素):实现Element接口,具体定义了接受访问者的方法,即accept(Visitor visitor)
方法。
ObjectStructure(对象结构):包含了要被访问的元素的集合,可以是一个集合、列表、树等结构。它提供了迭代器或遍历方法,以便访问者可以遍历访问其中的元素。
二、案例
这里举一个不是很恰当的例子:假设我们有一个电商网站,需要为不同类型的商品计算折扣价格。我们可以使用访问者模式来实现这个功能。
实现类图如下所示
1、案例实现
首先,我们定义一个商品接口(Element):
public interface Product {
void accept(Visitor visitor);
}
然后,我们创建两种具体商品类实现商品接口:
public class Book implements Product {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
public String getTitle() {
return title;
}
public double getPrice() {
return price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Clothing implements Product {
private String type;
private double price;
public Clothing(String type, double price) {
this.type = type;
this.price = price;
}
public String getType() {
return type;
}
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);
}
然后,我们创建一个具体访问者类(DiscountVisitor),实现访问者接口,并根据商品类型计算折扣价格:
public class DiscountVisitor implements Visitor {
private static final double BOOK_DISCOUNT = 0.1;
private static final double CLOTHING_DISCOUNT = 0.2;
@Override
public void visit(Book book) {
double discountedPrice = book.getPrice() * (1 - BOOK_DISCOUNT);
System.out.println("Book: " + book.getTitle() + ", Discounted Price: " + discountedPrice);
}
@Override
public void visit(Clothing clothing) {
double discountedPrice = clothing.getPrice() * (1 - CLOTHING_DISCOUNT);
System.out.println("Clothing: " + clothing.getType() + ", Discounted Price: " + discountedPrice);
}
}
最后,我们创建一个对象结构(ProductStructure),添加具体商品,并将访问者应用于商品:
public class ProductStructure {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public void removeProduct(Product product) {
products.remove(product);
}
public void applyDiscount(Visitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
}
}
现在,我们可以使用访问者模式来计算商品的折扣价格:
public class Client {
public static void main(String[] args) {
ProductStructure productStructure = new ProductStructure();
productStructure.addProduct(new Book("Design Patterns", 50.0));
productStructure.addProduct(new Clothing("T-Shirt", 30.0));
Visitor visitor = new DiscountVisitor();
productStructure.applyDiscount(visitor);
}
}
运行结果
Book: Design Patterns, Discounted Price: 45.0
Clothing: T-Shirt, Discounted Price: 24.0
2、案例分析
在上面这个例子中,我们创建了两种具体的商品(Book和Clothing),并将它们添加到对象结构中。然后,我们创建一个具体访问者(DiscountVisitor),它根据商品类型计算折扣价格并输出。最后,我们调用对象结构的applyDiscount()
方法,将访问者应用于商品,计算并打印出折扣后的价格。
这个例子展示了如何使用访问者模式在不改变商品类结构的情况下,定义新的操作(计算折扣价格)并应用于商品。通过将具体的计算逻辑封装在访问者中,我们可以轻松地添加新的访问者实现来执行其他操作,而不需要修改商品类的代码。
三、源码中的访问者模式
1、NIO中的FileVisitor
在研究FileVisitor之前,先看一下它的使用方式,用一段代码来介绍下,以下是一段实现了递归删除某个目录的代码
public class Main {
public static void main(String[] args) throws IOException {
Path start = Paths.get("C:\\Users\\kdyzm\\Downloads\\sdkdemo\\demo");
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e)
throws IOException {
if (e == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
});
}
}
我们来分析下这段代码:递归删除目录,需要首先看到文件就删除,文件都删除完了以后外层的文件夹也要删掉,按照这个逻辑删除完成之后,整个文件夹也就可以删掉了。
Files.walkFileTree
是个递归遍历文件夹的方法,SimpleFileVisitor
类继承于FileVisitor
接口,它的定义如下
public interface FileVisitor<T> {
//访问目录前之前执行的操作
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;
//访问文件时执行的操作
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;
//访问文件失败时执行的操作
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;
//访问目录后执行的操作
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
这个接口定义了四个方法,这四个方法就像是我们说的“钩子”,在某个事件发生的时候会触发调用。
从FileVisitor这个名字上来看,这个类使用了访问者模式,看下它的类图
从类图上来看,它和访问者模式的通用类图有些相似,但是却不一样:Path不是接口类,Files如果是ObjectStructure,那它应该维护一个Path列表才对。。这个问题解释起来还是以前的话:设计模式在实践的过程中几乎不会按照它的通用写法去实践,最重要的是实现它的精髓。
案例分析:
毫无疑问,从FileVisitor这个命名方式来看,确实使用了访问者模式,再看下FileVisitor的定义
public interface FileVisitor<T> {
//访问目录前之前执行的操作
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;
//访问文件时执行的操作
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;
//访问文件失败时执行的操作
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;
//访问目录后执行的操作
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
该接口要接收一个泛型<T>
,在上面的示例中,它实际上就是Path
,也就是Element
,在walkFileTree方法签名中也可以看到
FileVisitor的泛型必须是Path或其超类。然而扮演ObjectsStructure角色的Files类似乎“不称职”,因为它没有维护Path列表。那该怎么办?
Path类是“稳定的类”,我们想对它做多余的操作,例如“递归删除”,不应当影响它的类结构,所以这里使用了访问者模式。
参考文章:https://blog.csdn.net/scoryy/article/details/123667176 END.
注意:本文归作者所有,未经作者允许,不得转载