Bug辉的博客 不忘初心,方得始终!

GoF设计模式 - 访问者模式

2017-08-16
Bug辉 [原创文章]

本文为博主原创文章,请遵守文章最后的版权申明。


访问者模式(Visitor),行为型模式。GoF设计模式中结构最复杂的一种!将对相对固定的数据结构的操作封装,实现对数据结构的操作与数据结构本身分离,方便扩展对数据结构的新操作。略抽象是不?没关系,且看我分析。

《GoF设计模式 - 概述》一文中已经讲述了设计原则等基础知识,如果还没有看,请先看完这篇文章

不知不觉,设计模式系列的文章就要写完了。本文将是设计模式系列文章的最后一篇,希望今天能给这段时间来的努力画上一个完美的句号!

访问者模式

访问者模式

上面这个类图是我根据访问者模式的结构自己画的,可能与网络中流传的其他类图并不完全一致。根据实际的类关系,我给类图补充了一些关系箭头。也就是说,上面这张图其实是比目前我所看到的网络上流传的其他图片要更完整。

角色以及职责

  1. Element: 字面意思是元素的意思。在这里是被访问者的抽象,也就是我在文章开头说的数据结构的抽象。
  2. ConcreteElement: Element 的实现类,即具体的被访问者。
  3. Visitor: 访问者的抽象。所谓的访问者就是对Element的具体操作进行封装的对象。通常Visitor是一个接口。
  4. ConcreteVisitor: Visitor 的实现类,即具体的访问者。
  5. ObjectStructure: 结构对象。可以认为是Element这个数据结构的持有者。通常情况下是一个容器。

适用场景

对相对固定的数据结构进行操作,且希望能够很方便的对这个数据结构具备的操作进行扩展。用上面的类图来解释就是希望Element对应的数据结构的操作可以非常灵活的扩展,同时Element派生出来的子类需要相对固定,因为这个改变会导致Visitor接口需要跟着修改。

实例

本文将通过一个“文件浏览器”来演示访问者模式。这个“文件浏览器”将保存一个树形的文件结构,也就是目录树。这个目录树就对应着我上面说的那个相对固定的“数据结构”。文件浏览器本身将支持显示目录树和搜索文件两种功能,这两种功能就是上面说的对Element的操作。另外,如果需要新的功能的话,只需要扩展Visitor,而不需要改动数据结构本身。

访问者模式(Visitor)是GoF设计模式中结构最复杂的,所以理解起来会麻烦一些,请保持耐心!

类图

访问者模式

tips: 如果觉得上面的图字太小看不清可以戳图片去看大图。

编码实现

首先定义好我们的数据结构。在本例中,文件浏览器操作的对象是文件,所以操作的数据结构是目录树。目录树中每一个节点在这里我都将其抽象为一个FileInfo类。并且节点有两种可能的类型,一个是普通文件,一个是目录。分别对应着RegularFileInfoDirectoryInfo两个类。下面是代码。

/**
 * 文件描述信息
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public abstract class FileInfo {
    private String name;  //  文件名
    private String owner;  //  文件所有者
    private String path;  //  路径

    public FileInfo(){}

    public FileInfo(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    /**
     * 用访问者遍历文件树
     * @param visitor 访问者
     */
    public abstract void traverseFileTree(FileInfoVisitor visitor);
}
/**
 * 普通文件的描述信息
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class RegularFileInfo extends FileInfo {
    private byte[] data;  //  文件内容

    public RegularFileInfo(String name) {
        super(name);
    }

    @Override
    public void traverseFileTree(FileInfoVisitor visitor) {
        visitor.visitFile(this);
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "RegularFileInfo{" +
                "data=" + Arrays.toString(data) +
                '}';
    }
}
/**
 * 目录描述信息
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class DirectoryInfo extends FileInfo {
    private List<FileInfo> children;

    public DirectoryInfo(String path) {
        String[] pathParts = path.split("/");
        this.setName(pathParts[pathParts.length - 1]);
        this.setPath(path);
        children = new LinkedList<>();
    }

    @Override
    public void traverseFileTree(FileInfoVisitor visitor) {
        visitor.visitFile(this);
        for(FileInfo file : children){
            file.traverseFileTree(visitor);
        }
    }

    /**
     * @return 子节点总数
     */
    public int countChildren() {
        return children.size();
    }

    /**
     * 往目录中添加文件
     * @param fileInfo 待添加文件的描述信息
     */
    public void add(FileInfo fileInfo) {
        children.add(fileInfo);
        fileInfo.setPath(getPath() + "/" + fileInfo.getName());
    }

    /**
     * 从目录中移除文件
     * @param fileInfo 待移除文件
     */
    public void remove(FileInfo fileInfo) {
        children.remove(fileInfo);
    }

    /**
     * 清空目录
     */
    public void clear() {
        children.clear();
    }

    @Override
    public String toString() {
        return "DirectoryInfo{" +
                "path='" + getPath() + '\'' +
                '}';
    }
}

接下来是Visitor的抽象以及两个实现类。所谓Visitor即封装了对上述数据结构进行操作的对象。相应的,ConcreteVisitor 在本文中就是封装了文件搜索功能和目录树显示功能的两个对象。以下是代码。

/**
 * 文件访问者
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public interface FileInfoVisitor {
    /**
     * 访问普通文件
     * @param file 当前文件
     */
    void visitFile(RegularFileInfo file);

    /**
     * 访问目录
     * @param directory 当前目录
     */
    void visitFile(DirectoryInfo directory);
}
/**
 * 目录结构显示Visitor
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class DirectoryStructureDisplayer implements FileInfoVisitor {
    private int basePathDepth;

    public DirectoryStructureDisplayer(int basePathDepth) {
        this.basePathDepth = basePathDepth;
    }

    @Override
    public void visitFile(RegularFileInfo file) {
        int depth = file.getPath().split("/").length - basePathDepth - 1;
        for(int i = 0; i < depth; i++){
            System.out.printf("    ");
        }
        if (depth > 0){
            System.out.printf("|--");
        }
        System.out.println(file.getName());
    }

    @Override
    public void visitFile(DirectoryInfo directory) {
        int depth = directory.getPath().split("/").length - basePathDepth - 1;
        for(int i = 0; i < depth; i++){
            System.out.printf("    ");
        }
        if (depth > 0){
            System.out.printf("|--");
        }
        System.out.println(directory.getName() + "/");
    }
}
/**
 * 文件搜索Visitor
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class FileSearcher implements FileInfoVisitor {
    private Pattern fileNamePattern;

    public FileSearcher(String fileNamePattern) {
        this.fileNamePattern = Pattern.compile(fileNamePattern);
    }

    @Override
    public void visitFile(RegularFileInfo file) {
        if (fileNamePattern.matcher(file.getPath()).find()){
            System.out.println(file.getPath());
        }
    }

    @Override
    public void visitFile(DirectoryInfo directory) {
        if (fileNamePattern.matcher(directory.getPath()).find()){
            System.out.println(directory.getPath());
        }
    }
}

然后在实现文件浏览器类。这个类对应着访问者模式中的ObjectStructure

/**
 * 文件管理器
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class FileExplorer {
    private DirectoryInfo currentDirectory;  //  当前目录

    public DirectoryInfo getCurrentDirectory() {
        return currentDirectory;
    }

    public void setCurrentDirectory(DirectoryInfo currentDirectory) {
        this.currentDirectory = currentDirectory;
    }

    /**
     * 从当前所在目录开始用访问者遍历文件树
     * @param visitor 访问者
     */
    public void traverseFileTree(FileInfoVisitor visitor){
        currentDirectory.traverseFileTree(visitor);
    }
}

最后是客户端

/**
 * Client
 * @author: Elvin Zeng
 * @date: 17-8-16.
 */
public class App {
    public static void main(String[] args) {
        FileExplorer fileExplorer = new FileExplorer();
        fileExplorer.setCurrentDirectory(generateTestDir());

        System.out.println("显示目录结构 ");
        fileExplorer.traverseFileTree(new DirectoryStructureDisplayer(2));

        System.out.println("\n");
        System.out.println("=======================");
        System.out.println("搜索名称包含'.rmvb'的文件");
        fileExplorer.traverseFileTree(new FileSearcher("\\.rmvb"));
    }

    /**
     * @return 用于测试的目录结构
     */
    private static DirectoryInfo generateTestDir(){
        DirectoryInfo homeDirectory = new DirectoryInfo("/home/elvin");
        RegularFileInfo file1 = new RegularFileInfo("MySql从删库到跑路.pdf");
        homeDirectory.add(file1);

        DirectoryInfo videoDir = new DirectoryInfo("/home/elvin/video");
        homeDirectory.add(videoDir);
        RegularFileInfo videoFile1 = new RegularFileInfo("新年第一天.avi");
        videoDir.add(videoFile1);
        RegularFileInfo videoFile2 = new RegularFileInfo("生日.rmvb");
        videoDir.add(videoFile2);
        DirectoryInfo videoDir2 = new DirectoryInfo("/home/elvin/video/不可描述");
        videoDir.add(videoDir2);
        RegularFileInfo videoFile3 = new RegularFileInfo("事務室の一日.rmvb");
        videoDir2.add(videoFile3);
        RegularFileInfo videoFile4 = new RegularFileInfo("Bug輝の一日.rmvb");
        videoDir2.add(videoFile4);

        DirectoryInfo photoDir = new DirectoryInfo("/home/elvin/photo");
        homeDirectory.add(photoDir);
        RegularFileInfo photo1 = new RegularFileInfo("除夕全家福.png");
        photoDir.add(photo1);

        return homeDirectory;
    }

}

执行结果

$ java -jar visitor-1.0-SNAPSHOT.jar
显示目录结构
elvin/
    |--MySql从删库到跑路.pdf
    |--video/
        |--新年第一天.avi
        |--生日.rmvb
        |--不可描述/
            |--事務室の一日.rmvb
            |--Bug輝の一日.rmvb
    |--photo/
        |--除夕全家福.png


=======================
搜索名称包含'.rmvb'的文件
/home/elvin/video/生日.rmvb
/home/elvin/video/不可描述/事務室の一日.rmvb
/home/elvin/video/不可描述/Bug輝の一日.rmvb

访问者模式分析

访问者模式从某种程度上可以理解为是一个增强版的策略模式状态模式。同样是利用多态性来实现过程的动态绑定。 只是,之前的设计模式相对简单一些,仅仅只是用到了继承方式产生的多态性,也就是override。而访问者模式则同时利用overrideoverload实现了双分派,即同时利用方法的静态绑定和动态绑定。 通过这种手段实现了一个异常强大的特性——不仅对数据的操作可以动态切换而且同时支持不同的数据类型。在本例中,我们对目录树的操作可以随意扩充,只需要实现FileInfoVisitor接口即可。比如,现在我需要创建一个统计文件占用的磁盘空间大小的功能,这个时候我们只需要再创建一个实现FileInfoVisitor接口的类即可,目录树本身不用做任何改动。于此同时,目录树的节点本身也是可以支持不同类型的。这里说的不同类型在本例中则是普通文件和目录这两种类型。

但是强大的访问者模式也有一些缺点。首先最显而易见的就是结构本身有一定的复杂性。当问题的规模再大一些之后,恐怕接手维护这些代码的人会不容易理解这个设计。另一个大问题则是数据结构本身的类继承体系如果改动的话则需要进行比较大范围的改动。文章前面已经强调过了,访问者模式只适合用在数据结构相对稳定的问题中。举个例子来说,假设现在已经创建了10个访问者实现类。这时候我们希望增加以一种文件类型,假设这个文件类型叫“设备”。这个时候,则需要修改Visitor接口本身,增加对设备这个目录树节点类型的支持。并且,还需要修改现有的10个实现类,每个访问者实现类都得增加对设备文件的支持。这是违反开闭原则的操作,非常不妥。综上,采用访问者模式一定要考虑清楚!

参考

后记

二十四个设计模式(算上简单工厂),总共二十五篇文章,一共花了我两个多月时间。终于写完了!我已经将我掌握的知识都共享出来了,如果有认可我的付出的同学,可以考虑戳下面的打赏给我鼓励!╭(′▽`)╯
设计模式本身并没有什么难度,可是真正去写一下这些文章,并为每篇文章配好图和例子,就会发现还是有一定难度的。 虽然写这种文章对现在的我来说,知识上并没有增长多少。但是这个过程,很好的训练了我的执行力和毅力!

接下来的日子,我会稍微休整一下,专心准备工作问题。


版权声明

知识共享许可协议
若无特殊说明则文章内容均为博主原创内容,包括但不限于代码、文字、图片。
《GoF设计模式 - 访问者模式》 Bug辉 采用 知识共享 署名-非商业性使用-禁止演绎 4.0 国际 许可协议 进行许可。
转载文章时文章署名请注明原作者为Bug辉(Elvin Zeng、zenghui、曾辉也行)并带上原文链接:https://www.bughui.com/2017/08/16/gof-design-pattern-visitor/

鉴于目前国人版权意识比较薄弱,所以路过的同学可能并不了解CC协议。在此特地介绍一下!上述协议大概的意思是(这里只是简单解释,并不替代协议,以上述协议为准):
  • 权利:遵守协议的情况下你可以在任何媒介以任何形式复制、发行本作品。
  • 约束:使用本作品得保留署名(作者+原文链接),不得声称或暗示文章是你创作的。
  • 约束:你不可以将本作品用于商业目的。
  • 约束:如果你再混合、转换、或者基于本作品创作,你不可以发布修改后的作品。
  • 在得到作者允许的情况下你可以不用受上述条款约束。
不论本作品是否对你有益,不论你是否认同本作品的观点,本作品都是作者的劳动产物。尊重别人的劳动别人才会尊重你的劳动是吧!


类似文章

评论

打赏时请在备注信息中填上你的称呼!好让我把你的名字加入致谢名单