面向对象编程内功心法系列十七(访问者模式:单分派or双分派)

134 阅读6分钟

1.引子

有一阵子没有分享文章了,确实是这段时间事情比较多,今天难得有空,我们分享一段,今天我要给你分享的是关于单分派(Single Dispatch),双分派(Double Dispatch)。

什么是单分派?

单分派的定义是,调用哪个对象(多态) 的方法,在运行期确定;调用对象的哪个方法(方法重载) ,在编译期确定

什么是双分派?

双分派的定义是,调用哪个对象的方法,在运行期(多态) 确定;调用对象的拿个方法(方法重载),在运行期确定

那么问题来了?你熟悉的java编程语言,是单分派呢?还是双分派?

2.案例

2.1.java是单分派,还是多分派?

我们先看一个案例,看看java是单分派,还是多分派?一段示例代码,有四个类:

  • Parent:父类
  • Child:子类
  • Dispatch:演示分派特性类
  • Main:执行入库口类

2.1.1.Parent

package cn.edu.anan.pattern.dispatch;
​
/**
 * 父类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 9:51
 */
public class Parent {
​
    public void f(){
        System.out.println("I am Parent.f()");
    }
}

2.1.2.Child

package cn.edu.anan.pattern.dispatch;
​
/**
 * 子类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 9:52
 */
public class Child extends Parent {
​
    @Override
    public void f() {
        System.out.println("I am Child.f()");
    }
}

2.1.3.Dispatch

package cn.edu.anan.pattern.dispatch;
​
/**
 * 单分派,还是双分派?
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:05
 */
public class Dispatch {
​
    /**
     * 调用哪个对象的方法,在运行期确定
     * @param p
     */
    public void dispatchFun(Parent p){
        p.f();
    }
​
    /**
     * 重载方法1.调用对象的哪个方法,(方法参数)在编译期确定
     * @param p
     */
    public void overloadFun(Parent p){
        System.out.println("I am overloadFun.Parent");
        p.f();
    }
​
    /**
     * 重载方法2.调用对象的哪个方法,(方法参数)在编译期确定
     * @param c
     */
    public void overloadFun(Child c){
        System.out.println("I am overloadFun.Child");
        c.f();
    }
}

2.1.4.Main

package cn.edu.anan.pattern.dispatch;
​
/**
 * 测试主类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 9:55
 */
public class Main {
​
    /**
     * main
     * @param args
     */
    public static void main(String[] args) {
        // 多态:调用哪个对象的方法,在运行期确定
        // 左边=静态类型,方法重载以左边为准
        // 右边=实际类型,多态以右边为准
        Parent p = new Child();
        Dispatch dispatch = new Dispatch();
        dispatch.dispatchFun(p);
​
        // 方法重载:调用对象的哪个方法,(方法参数)在编译期确定
        System.out.println("-------------------分割线-----------------");
        dispatch.overloadFun(p);
​
    }
​
}

2.1.5.执行结果

D:\02teach\01soft\jdk8\bin\java cn.edu.anan.pattern.dispatch.Main
I am Child.f()
-------------------分割线-----------------
I am overloadFun.Parent
I am Child.f()
​
Process finished with exit code 0

从最终执行结果看到,调用哪个对象的方法,是在运行期确定,即多态特性;代用对象的哪个方法,是在编译期确定,即方法重载。

  • 代码行
 Parent p = new Child();
​
/**
 * 调用哪个对象的方法,在运行期确定
* @param p
 */
public void dispatchFun(Parent p){
    p.f();
}
​
/**
* 重载方法1.调用对象的哪个方法,(方法参数)在编译期确定
* @param p
*/
public void overloadFun(Parent p){
    System.out.println("I am overloadFun.Parent");
    p.f();
}
  • 执行结果
I am Child.f()

所以我们看到,java编程语言是单分派。

2.2.双分派,不需要访问者模式

2.2.1.什么是访问者模式

访问者模式是经典Gof 设计模式中,行为行设计模式的一种,它主要是用于将对象,与操作解耦的一种技术手段,两个角色

  • 访问者:操作
  • 被访问者:对象

从定义上看,比较难理解,太过于抽象了!我们通过一个案例来看。

假设在实际应用中,有这么一个需求

#1.文件处理,针对不同的文件格式,比如说:PPTWORDEXCELPDFHTMLXMLYAML#2.需要解析不同类型文件,获取文件内容,转换成统一的文件格式,比如String结果内容
#3.需要将文件内容,进行压缩处理
#4.需要将不同类型文件,进行xxx处理
#5.等等,总的来说,需要根据上层业务需要,进行各种业务处理
​
#6.问题来了,你该如何实现呢?当然我们知道,单纯实现文件处理的功能不难,难的是如何应对业务扩展,让代码设计实现上满足开闭原则。总不能业务上加一个业务需求,又要到处修改代码吧?

面对这个业务需求场景,正好是访问者模式的应用场景了。它的两个角色

  • 访问者:操作,即业务处理(解析、压缩等)
  • 被访问者:对象,即各种类型文件

明白了业务场景,下面来看代码实现。我以PPT、和WORD类型文件为例。

2.2.1.1.定义被访问者

FileVisitable

package cn.edu.anan.pattern.visitor;
​
/**
 * 被访问者抽象
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:45
 */
public abstract class FileVisitable {
​
    protected String path;
​
    public FileVisitable(String path){
        this.path = path;
    }
​
    /**
     * 接受访问入口
     * @param visitor
     */
    public abstract void accept(FileVisitor visitor);
}

PPTFile

package cn.edu.anan.pattern.visitor;
​
/**
 * PPT类型文件
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:47
 */
public class PPTFile extends FileVisitable {
​
    public PPTFile(String path){
        super(path);
    }
​
    /**
     * 接受访问入口
     *
     * @param visitor
     */
    @Override
    public void accept(FileVisitor visitor) {
        visitor.visit(this);
    }
}

WORDFile

package cn.edu.anan.pattern.visitor;
​
/**
 * WORD类型文件
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:48
 */
public class WORDFile extends FileVisitable{
​
    public WORDFile(String path){
        super(path);
    }
​
    /**
     * 接受访问入口
     *
     * @param visitor
     */
    @Override
    public void accept(FileVisitor visitor) {
        visitor.visit(this);
    }
}
2.2.1.2.定义访问者

FileVisitor

package cn.edu.anan.pattern.visitor;
​
/**
 * 访问者接口
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:46
 */
public interface FileVisitor {
​
    /**
     * 访问ppt类型文件
     * @param pptFile
     */
    void visit(PPTFile pptFile);
​
    /**
     * 访问word类型文件
     * @param wordFile
     */
    void visit(WORDFile wordFile);
}

ParseFileVisitor

package cn.edu.anan.pattern.visitor;
​
/**
 * 文件内容解析访问者
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:51
 */
public class ParseFileVisitor implements FileVisitor {
​
    /**
     * 访问ppt类型文件
     *
     * @param pptFile
     */
    @Override
    public void visit(PPTFile pptFile) {
        System.out.println("解析文件内容,文件类型:PPTFile");
    }
​
    /**
     * 访问word类型文件
     *
     * @param wordFile
     */
    @Override
    public void visit(WORDFile wordFile) {
        System.out.println("解析文件内容,文件类型:WORDFile");
    }
}
2.2.1.3.应用
package cn.edu.anan.pattern.visitor;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * 执行入口
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 10:53
 */
public class Main {
​
    public static void main(String[] args) {
​
        // 1.待处理文件列表
        List<FileVisitable> files = new ArrayList<FileVisitable>();
        files.add(new PPTFile("my.ppt"));
        files.add(new WORDFile("my.doc"));
​
        // 2.解析文件内容
        FileVisitor visitor = new ParseFileVisitor();
        for(FileVisitable f : files){
            f.accept(visitor);
        }
​
    }
​
}

image.png

2.2.2.双分派假设

从上面的案例我们看到,访问者模式带来的最大收益是

  • 解决扩展性的问题,它将被访问者(对象),与访问者(操作)解耦,使得代码设计实现上满足开闭原则
  • 通常被访问者业务对象,范围是相对明确的,不太需要关注扩展问题
  • 主要是访问者操作,业务需求是灵活多变的:解析内容,压缩处理等等,需要关注扩展问题
  • 通过访问者模式,比如我们上面的案例需要增加压缩处理,只需要再扩展实现一个FileVisitor即可,其它代码不需要修改
  • 这就是我们所期望的,满足开闭原则,这样一来测试同学轻松了,代码质量高了,线上bug少了。大家都很Happy!

但是,从上面的访问者模式代码实现来看,它也是有成本的

  • 代码结构比较绕,在业务对象中,需要提供访问者入口
/**
* 接受访问入口
*
* @param visitor
*/
@Override
public void accept(FileVisitor visitor) {
    visitor.visit(this);
}
  • 代码结构可读性比较差,不容易看懂,尤其对于刚入门的小伙伴!

    那么,假设我们使用的编程语言支持双分派,即方法重载支持运行期确定的话,只需要定义一个工具类,通过方法重载封装各业务操作,不需要在对象中再提供访问者的入口了

    即我们这里说的,支持双分派,则不需要访问者模式。