设计模式学习笔记 - 设计模式与范式 -行为型:13.访问者模式(下):为什么支持双分派的语言不需要访问者模式

概述

上篇文章,我们学习了访问者模式的原理和实现,并还原了访问者模式诞生的过程。总体来说,这个模式的代码实现比较难,所以应用场景不多。从应用开发的角度来说,它的确不是我们学习的重点。

本章,我们把访问者模式作为引子,一块讨论下这样两个问题:

  • 为什么支持双分派的语言不需要访问者模式?
  • 除了访问者模式,上篇文章的例子还有其他的实现方式吗?

为什么支持双分派的语言不需要访问者模式?

实际上,讲到访问者模式,大部分书籍或资料都会讲到 Double Dispatch,中文翻译为双分派。虽然学习访问者模式,并不用非得理解这个概念,但是为了让你在查看其他书籍或资料时,不会卡在这个概念上,本章在这里讲一下。

此外,个人觉得,学习 Double DIspatch 可以加深你对访问者模式的理解。

既然有 Double Dispatch,对应的就有 Signle Dispatch。

  • 所谓 Signle Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。
  • 所谓 Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。

如何理解 “Dispatch” 这个单词呢? 在面向对象编程语言中,可以把方法调用理解为一种消息传递,也就是 “Dispatch”。一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息要包含对象名、方法名、方法参数。

如何理解 “Single” “Double” 这两个单词呢? “Single” “Double” 指的是执行那个对象的哪个方法,跟几个因素的运行时类型有关。

  • Signle Dispatch 之所以称为 “Single”,是因为执行哪个对象的哪个方法,只跟 “对象” 的运行时类型有关。
  • Double Dispatch 之所以称为 “Double”,是因为执行哪个对象的哪个方法,跟 “对象” 和 “方法参数” 两者的运行时类型有关。

具体到编程语言的语法机制,Signle Dispatch 和 Double Dispatch 跟多态和函数重载直接相关。当前主流的面向对象编程语言(比如,Java、C++、C#)都只支持 Signle Dispatch ,不支持 Double Dispatch。

接下来,拿 Java 语言来举例说明下。

Java 支持多态,代码可以在运行时获得对象的实际类型(也就是前面提到的运行时类型),然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载,并不是在运行时,根据传递进函数的参数的实际类型,来决定调用重载函数。而是在编译时,根据传递进函数的参数的声明类型(也就是前面提到的编译时类型),来决定调用哪个重载函数。也就是说,具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟函数参数的运行时类型无关。所以,Java 语言只支持 Signle Dispatch 。

这么说可能比较抽象,下面举个例子来说明下。

public class ParentClass {
    public void f() {
        System.out.println("I am ParentClass's f().");
    }
}

public class ChildClass extends ParentClass {
    @Override
    public void f() {
        System.out.println("I am ChildClass's f().");
    }
}

public class SingleDispatchCLass {
    public void polymorphismFunction(ParentClass p) {
        p.f();
    }

    public void overloadsFunction(ParentClass p) {
        System.out.println("I am overloadFunction(ParentClass p)");
    }

    public void overloadsFunction(ChildClass c) {
        System.out.println("I am overloadFunction(ChildClass c)");
    }
}

public class DemoMain {
    public static void main(String[] args) {
        SingleDispatchCLass demo = new SingleDispatchCLass();
        ParentClass p = new ChildClass();
        demo.polymorphismFunction(p); // 执行哪个对象的方法,由对象的实际类型决定
        demo.overloadsFunction(p); // 执行对象的哪个方法,由参数对象的声明类型决定
    }
}

// 代码执行结果:
I am ChildClass's f().
I am overloadFunction(ParentClass p)

上面的代码中,demo.polymorphismFunction(p) 执行 p 的实际类型的 f() 函数,也就是 ChildClassf() 函数。 demo.overloadsFunction(p) 匹配的是重载函数中的 overloadsFunction(ParentClass p) ,也就是根据 p 的声明类型来决定匹配哪个重载函数。

假设 Java 语言支持 Double Dispatch,那下面的代码(摘抄至上篇文章)中 extractor.extract2txt(resourceFile) 的就不会报错。代码运行时,根据参数(resourceFile)的实际类型(PdfFilePptFileWordFile),来决定使用 extract2txt 的三个重载函数中的哪一个。下面的代码就能正常运行了,也就不需要访问者模式了。这也回达了为什么支持 Double Dispatch 的语言不需要访问者模式。

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;
    }
}

除了访问者模式,上一节的例子还有其他的实现方案吗?

上篇文章,通过一个例子给你展示了,访问者模式是如何一步一步设计出来的。我们在回顾下那个例子。我们从网址站上爬取了很多资源文件,它们的格式有:PDF、PPT、Word。我们要开发一个工具来处理这批资源文件,这其实就包含抽取文本内容、压缩资源文件、提取文件信息等。

实际上,开发这个工具有很多种代码设计和实现思路。为了讲解访问者模式,上篇文章,我们使用了访问者模式来实现。实际上,还有其他的实现方法,比如,可以利用工程模式来实现,定义一个包含 extract2txt() 函数的 Extractor 接口。PdfExtractorPptExtractorWordExtractor 实现 Extractor 接口,并且在各自的 extract2txt() 函数中,分别实现 pdf、ppt、word 格式文件的文本内容抽取 。ExtractorFactory 工厂类根据不同的文件类型,返回不同的 Extractor

这个实现思路其实更加简单,代码如下所示。

public enum ResourceFileType {
    PDF,
    PPT,
    WORD;
}

public abstract class ResourceFile {
    protected String filePath;

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

    public abstract ResourceFileType getType();
}

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

    @Override
    public ResourceFileType getType() {
        return ResourceFileType.PDF;
    }
    // ...
}

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

    @Override
    public ResourceFileType getType() {
        return ResourceFileType.PPT;
    }
    // ...
}

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

    @Override
    public ResourceFileType getType() {
        return ResourceFileType.WORD;
    }
    // ...
}

public interface Extractor {
    void extract2txt(ResourceFile resourceFile);
}

public class PdfExtractor implements Extractor {
    @Override
    public void extract2txt(ResourceFile resourceFile) {
        // ...
    }
}

public class PptExtractor implements Extractor {
    @Override
    public void extract2txt(ResourceFile resourceFile) {
        // ...
    }
}

public class WordExtractor implements Extractor {
    @Override
    public void extract2txt(ResourceFile resourceFile) {
        // ...
    }
}

public class ExtractorFactory {
    private static final Map<ResourceFileType, Extractor> extractors = new HashMap<>();
    static {
        extractors.put(ResourceFileType.PDF, new PdfExtractor());
        extractors.put(ResourceFileType.PPT, new PptExtractor());
        extractors.put(ResourceFileType.WORD, new WordExtractor());
    }

    public static Extractor getExtractor(ResourceFileType type) {
        return extractors.get(type);
    }
}

public class ToolApplication {
    public static void main(String[] args) {
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
            Extractor extractor = ExtractorFactory.getExtractor(resourceFile.getType());
            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;
    }
}

当需要添加新功能时,比如压缩文件,类似抽取文本内容功能的实现代码,只需要添加一个 Compressor 接口,PdfCompressorPptCompressorWordCompressor 三个实现类,以及创建它们的 CompressorFactory 工厂类即可。唯一需要修改的只有最上层的 ToolApplication。基本上符合 “对扩展开放、对修改关闭” 的设计原则。

  • 对于资源文件处理工具的例子,如果工具提供的功能并不是很多,只有几个而已,那更推荐使用工程模式的实现方式,比较代码清晰、易懂。
  • 相反,如果工具提供非常多的功能,比如有十几个,那更推荐使用访问者模式,因为访问者模式需要定义的类要比工程模式的少很多,类太多也会影响代码的可维护性。

总结

总体来说,访问者模式难以理解,应用场景有限,不是特别必需,不建议在项目中使用它。所以,对于上篇文章的处理资源文件的例子,更推荐使用工厂设计模式来设计和实现。

本章重点讲解了 Double Dispatch。在面向对象编程语言中,方法调用可以理解为一种消息传递(Dispatch)。一个对象调用另一个对象的方法,就相当于给它发送一条消息,这条消息起码要包含对象名、方法名和方法参数。

  • 所谓 Signle Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。
  • 所谓 Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。

具体到编程语言的语法机制,Signle Dispatch 和 Double Dispatch 跟多态和函数重载直接相关。当前主流的面向对象编程语言(如,Java、C++)都只支持 Signle Dispatch,不支持 Double Dispatch。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-15 02:24:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-15 02:24:02       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-15 02:24:02       82 阅读
  4. Python语言-面向对象

    2024-04-15 02:24:02       91 阅读

热门阅读

  1. 使用 bert-base-chinese-ner 模型实现中文NER

    2024-04-15 02:24:02       33 阅读
  2. Golang实践:用Sync.Map实现简易内存缓存系统

    2024-04-15 02:24:02       37 阅读
  3. Sql缺失索引查询,自动创建执行语句

    2024-04-15 02:24:02       38 阅读
  4. 【SpinalHDL】Scala编程中的class及case class

    2024-04-15 02:24:02       40 阅读
  5. vue 插槽使用

    2024-04-15 02:24:02       39 阅读
  6. HTML的基本结构

    2024-04-15 02:24:02       36 阅读
  7. MongoDB聚合运算符:$push

    2024-04-15 02:24:02       38 阅读
  8. 数据库-Redis(11)

    2024-04-15 02:24:02       28 阅读
  9. [蓝桥杯 2023 国 B] 班级活动

    2024-04-15 02:24:02       41 阅读
  10. Linux系统命令三剑客awk

    2024-04-15 02:24:02       37 阅读