设计模式(十四):访问者模式

Published on 2024-03-25 15:03 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、访问者模式定义

访问者模式是一种行为型模式,而且是行为型模式中比较复杂的一种模式。

访问者模式(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这个名字上来看,这个类使用了访问者模式,看下它的类图

NIO中的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方法签名中也可以看到

image-20240322184343691

FileVisitor的泛型必须是Path或其超类。然而扮演ObjectsStructure角色的Files类似乎“不称职”,因为它没有维护Path列表。那该怎么办?

Path类是“稳定的类”,我们想对它做多余的操作,例如“递归删除”,不应当影响它的类结构,所以这里使用了访问者模式。



参考文章:https://blog.csdn.net/scoryy/article/details/123667176 END.


#设计模式
目录