设计模式学习笔记 - 设计模式与范式 -行为型:12.访问者模式(上):带你还原访问者模式的诞生过程

概述

本章要讲的访问者模式,可以算是 23 种设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式几乎在实际的开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。

为了让你以后读到应用了访问者模式代码时,能一眼就看出代码的设计意图,还是有必要和你讲一下这个设计模式。此外,本章还会带你还原访问者模式诞生的思维过程,让你切身感受到创造一种新的设计模式,并不是一件很难的事情。


带你 "发明" 访问者模式

假设我们从网站上爬取了很多资源文件,它的格式有三种:PDF、PPT、Word。现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。如果让你来实现,你会怎么来做呢?

实现这个功能并不难,也有很多的实现方式,我将其中的一种代码实现贴在下面。其中 ResourceFile 是一个抽象类,包含一个抽象函数 extract2txt()PdfFilePptFileWordFile 都继承 ResourceFile 类,并重写 extract2txt() 函数。在 ToolApplcation 中,我们利用多态特性,根据对象的实际类型,来决定执行哪个方法。

java 复制代码
public abstract class ResourceFile {
    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }

    public abstract void extract2txt();
}

public class PptFile extends ResourceFile {
    public PptFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // 省略一大坨从ppt中抽取文本的代码...
        // 将抽取出来的文本保持在跟filepath同名的.txt文件中...
        System.out.println("Extract PPT.");
    }
}

public class PdfFile extends ResourceFile {
    public PdfFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // ...
        System.out.println("Extract PDF.");
    }
}

public class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }

    @Override
    public void extract2txt() {
        // ...
        System.out.println("Extract Word.");
    }
}

public class ToolApplication {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.extract2txt();
        }
    }

    private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        // ... 根据后缀(ppt/pdf/word)由工厂方法创建不同类型的类对象(PptFile/PdfFile/WordFile)
        resourceFiles.add(new PdfFile("a.pdf"));
        resourceFiles.add(new PptFile("b.ppt"));
        resourceFiles.add(new WordFile("c.word"));
        return resourceFiles;
    }
}

如果工具的功能不停地扩展,不仅要能抽取文本内容,还要支持压缩、提取文件元信息(文件名、文件大小、更新时间等等),那如果继续使用上面的实现思路,就会存在以下几个问题:

  • 违反开闭原则,添加一个新功能,所有类的代码都要修改;
  • 虽然功能增多,每个类的代码不断膨胀,可读性和可维护性变差了。
  • 把所有比较上层的业务逻辑都耦合到 PdfFilePptFileWordFile 类中,导致这些类的职责不够单一,编程了大杂烩。

针对上面的问题,常用的解决办法就是拆分解耦,把业务跟具体的数据结构解耦,设计成独立的类。这里我们按照访问者模式的演进思路对上面的代码进行重构。重构之后的代码如下所示:

java 复制代码
public abstract class ResourceFile {
    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }
}

public class PptFile extends ResourceFile {
    public PptFile(String filePath) {
        super(filePath);
    }
    // ...
}

public class PdfFile extends ResourceFile {
    public PptFile(String filePath) {
        super(filePath);
    }
    // ...
}

public class WordFile extends ResourceFile {
    public PptFile(String filePath) {
        super(filePath);
    }
    // ...
}

public class Extractor {
    public void extract2txt(PptFile pptFile) {
        // ...
        System.out.println("Extract PPT.");
    }

    public void extract2txt(PdfFile pdfFile) {
        // ...
        System.out.println("Extract PDF.");
    }

    public void extract2txt(WordFile wordFile) {
        // ...
        System.out.println("Extract Word.");
    }
}

public class ToolApplication {
    public static void main(String[] args) {
        Extractor extractor = new Extractor();
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
            extractor.extract2txt(resourceFile);
        }
    }

    private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        // ... 根据后缀(ppt/pdf/word)由工厂方法创建不同类型的类对象(PptFile/PdfFile/WordFile)
        resourceFiles.add(new PdfFile("a.pdf"));
        resourceFiles.add(new PptFile("b.ppt"));
        resourceFiles.add(new WordFile("c.word"));
        return resourceFiles;
    }
}

这其中的关键就是,把抽取文本内容的操作,设计成了三个重载函数。函数重载是 Java、C++ 这类面向对象编程语音中常见的语法机制。所谓重载函数是指,在同一类中函数名相同,参数不同的一组函数。

不过,如果你细心的话,会发现 ToolApplicationextractor.extract2txt(resourceFile) 的代码会变异通过不了。这是为什么呢?

我们知道,多态是一种动态绑定,可以在运行时获取对象的类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。

List resourceFiles = listAllResourceFiles(args[0]);

for (ResourceFile resourceFile : resourceFiles) {

extractor.extract2txt(resourceFile);

}

ToolApplication 类中,上述几行代码中, resourceFiles 包含对象的声明类型都是 ResourceFile,而我们并没有在 Extractor 类中定义参数类型是 ResourceFileextract2txt() 重载函数,所以在编译阶段就通过不了,更别说在运行时根据对象的实际类型执行不同的重载函数了。

这个问题该如何解决呢?

解决办法稍微有点复杂,我们先来看代码,后面在慢慢解释。

java 复制代码
public abstract class ResourceFile {
    protected String filePath;

    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }

    public abstract void accept(Visitor visitor);
}

public class PptFile extends ResourceFile {
    public PptFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // ...
}

public class PdfFile extends ResourceFile {
    public PdfFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // ...
}

public class WordFile extends ResourceFile {
    public WordFile(String filePath) {
        super(filePath);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // ...
}

public interface Visitor {
    void visit(PdfFile pdfFile);
    void visit(PptFile pptFile);
    void visit(WordFile wordFile);
}

public class Extractor implements Visitor{
    @Override
    public void visit(PptFile pptFile) {
        // ...
        System.out.println("Extract PPT.");
    }

    @Override
    public void visit(PdfFile pdfFile) {
        // ...
        System.out.println("Extract PDF.");
    }

    @Override
    public void visit(WordFile wordFile) {
        // ...
        System.out.println("Extract Word.");
    }
}

public class Compressor implements Visitor {
    @Override
    public void visit(PdfFile pdfFile) {
        // ...
        System.out.println("Compress PDF.");
    }

    @Override
    public void visit(PptFile pptFile) {
        // ...
        System.out.println("Compress PPT.");
    }

    @Override
    public void visit(WordFile wordFile) {
        // ...
        System.out.println("Compress Word.");
    }
}

public class ToolApplication {
    public static void main(String[] args) {
        Extractor extractor = new Extractor();
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(extractor);
        }

        Compressor compressor = new Compressor();
        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(compressor);
        }
    }

    private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
        List<ResourceFile> resourceFiles = new ArrayList<>();
        // ... 根据后缀(ppt/pdf/word)由工厂方法创建不同类型的类对象(PptFile/PdfFile/WordFile)
        resourceFiles.add(new PdfFile("a.pdf"));
        resourceFiles.add(new PptFile("b.ppt"));
        resourceFiles.add(new WordFile("c.word"));
        return resourceFiles;
    }
}

重新来看访问者模式

刚刚带你一步步还原了访问者模式诞生的过程,现在,回过头来总结下,这个模式的实现和原理。

访问者模式的英文为 Visitor Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:

Allows for one or more operation to be applied to a set of objects at runtime,decouping the operations from the object structure.

翻译成中文就是:允许一个或者多个操作应用到一组对象上,解耦操作与对象本身

定义比较简单,结合前面的例子不难理解。对于访问者模式的代码实现,实际上,在上面的例子中,经过层层重构之后的最终代码,就是标准的访问者模式的实现代码。下图为一张类图,你可以对照着前面的例子一块看。

最后,再来总结下访问者模式的应用场景。

一般来说,访问者模式针对的是一组类型不同的对象(PdfFilePptFileWordFile)。不过尽管这组对象不同,但是它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,需要对这组对象进行一些列不同的业务操作(抽取文本、压缩等),为了避免不断添加功能导致类(PdfFilePptFileWordFile)不断膨胀,职责越来越不单一,以及避免频繁地修改代码,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(ExtractorCompressor)中。

总结

访问者模式,允许一个或者多个操作应用到一组对象上,设计意图是解耦操作与对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。

对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的原因是,函数重载在大部分面向对象编程语言重是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数声明类型决定的,而非运行时,根据参数的实际类型决定的。

正是因为代码实现难理解,所以,在项目中应用这种设计模式,会导致代码的可读性比较差。如果其他人不了解这种设计模式,可能会读不懂、维护不了你写的代码。所以,除非不得已,不要使用访问者模式

相关推荐
WangMing_X7 天前
C# 23种设计模式(4)访问者模式(Visitor Pattern)
开发语言·设计模式·c#·访问者模式
我码玄黄10 天前
JS设计模式之访问者模式
javascript·设计模式·访问者模式
博风11 天前
设计模式:24、访问者模式
设计模式·访问者模式
喵手18 天前
设计模式探秘:迭代器模式与访问者模式详解
设计模式·迭代器模式·访问者模式
橘色的喵19 天前
C++编程:模拟实现CyberRT的DataVisitor和DataDispatcher
c++·访问者模式·观察者·cyberrt·datavisitor·datadispatcher
小白不太白9501 个月前
设计模式之 访问者模式
java·设计模式·访问者模式
蓝田~1 个月前
访问者模式
访问者模式
萨达大1 个月前
23种设计模式-访问者(Visitor)设计模式
java·c++·设计模式·软考·访问者模式·软件设计师·行为型设计模式
丶白泽2 个月前
重修设计模式-行为型-访问者模式
java·设计模式·访问者模式·1024程序员节
努力找工作的OMArmy2 个月前
软件开发----设计模式每日刷题(转载于牛客)
java·单例模式·设计模式·策略模式·访问者模式·模板方法模式·开闭原则