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.文件处理,针对不同的文件格式,比如说:PPT、WORD、EXCEL、PDF、HTML、XML、YAML等
#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);
}
}
}
2.2.2.双分派假设
从上面的案例我们看到,访问者模式带来的最大收益是
- 解决扩展性的问题,它将被访问者(对象),与访问者(操作)解耦,使得代码设计实现上满足开闭原则
- 通常被访问者业务对象,范围是相对明确的,不太需要关注扩展问题
- 主要是访问者操作,业务需求是灵活多变的:解析内容,压缩处理等等,需要关注扩展问题
- 通过访问者模式,比如我们上面的案例需要增加压缩处理,只需要再扩展实现一个FileVisitor即可,其它代码不需要修改
- 这就是我们所期望的,满足开闭原则,这样一来测试同学轻松了,代码质量高了,线上bug少了。大家都很Happy!
但是,从上面的访问者模式代码实现来看,它也是有成本的
- 代码结构比较绕,在业务对象中,需要提供访问者入口
/**
* 接受访问入口
*
* @param visitor
*/
@Override
public void accept(FileVisitor visitor) {
visitor.visit(this);
}
-
代码结构可读性比较差,不容易看懂,尤其对于刚入门的小伙伴!
那么,假设我们使用的编程语言支持双分派,即方法重载支持运行期确定的话,只需要定义一个工具类,通过方法重载封装各业务操作,不需要在对象中再提供访问者的入口了。
即我们这里说的,支持双分派,则不需要访问者模式。