首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

多态、双分派与访问者模式

  • 25-04-17 09:21
  • 3232
  • 7503
juejin.cn

多态

参考维基百科,多态(polymorphism)指为不同数据类型的实体提供统一的接口[1],或使用一个单一的符号来表示多个不同的类型[2]。例如,方法重载就是多态的一种(也称为特设多态, ad hoc polymorphism),支持这种多态的语言可以定义同名,但不同参数列表的方法。调用时根据参数的类型来选择对应的方法进行调用,例如:

java
代码解读
复制代码
public static int add(Integer a, Integer b){ return a + b; } public static String add(String a, String b){ return a.concat(b); } public static void main(String[] args) { System.out.println(add(1, 2)); // 选择第一个 add 方法 System.out.println(add("hello", "world")); // 选择第二个 add 方法 }

这也达成了为不同的数据类型提供统一的接口这一目的。

还有一种常见的多态是子类型多态,即是通过继承/实现一个父类或接口的方式,构成多个子类型,每个子类型有不同的行为,但同名的方法,例如:

java
代码解读
复制代码
interface Animal{ void say(); } class Dog implements Animal{ @Override public void say() { System.out.println("旺旺"); } } class Cat implements Animal{ @Override public void say() { System.out.println("喵喵"); } } public static void saySomething(Animal animal){ animal.say(); } Animal cat = new Cat(); Animal dog = new Dog(); saySomething(cat); // 喵喵 saySomething(dog); // 旺旺

可以看出,虽然 saySomething 的方法签名类型为Animal,但最终是根据入参的实际类型(Dog或Cat)来调用对应的 say 方法。

静态多态与动态多态

在上面提到的方法重载与子类型都是实现多态的一种方式。但这里必须强调一下这两种方法的不同之处,方法重载是在编译时生效,因此又被称之为静态多态。而子类型则是在运行时生效,称之为动态多态。对于参数重载,我们无法写入下面的代码:

java
代码解读
复制代码
Object s1 = "123"; Object s2 = "234"; add(s1, s2);

这段代码会提示我们找不到 add(Object, Object) 这样的方法,因为方法重载必须在编译时寻找到参数签名完全一致的方法,否则就无法选择到底调用哪个方法。很多人可能会说,s1, s2 这么明显都是 String,明明可以从编译期推断出来,编译器为什么不能智能一点帮我们匹配上 add(String, String) 这个方法呢?因为上面的例子只是一个特例,而编译器完全可能面对下面的代码:

java
代码解读
复制代码
Object s1 = randInt(10) > 5 ? 1: "123"; Object s2 = randInt(10) > 5 ? 2: "456"; add(s1, s2);

可以看出,s1 与 s2 的类型需要运行时才能决定,因此方法重载无法处理这种情况。

而与之相对的,子类型多态则是在运行时才调用哪个对象的法,例如下面的代码是可以正常运行的:

java
代码解读
复制代码
// 必须要到运行时才能确定 Animal 的真正类型是 Dog 还是 Cat Animal animal = randInt(10) > 5 ? new Cat(): new Dog(); saySomething(animal);

单分派与双分派

首先,分派可以理解为实现多态的过程。例如对于方法重载,其根据参数选择正确的方法这一过程就可以称之为分派。 像 java, c++ 原生只支持单分派,即要么在编译时通过方法重载来实现多态,要么在运行时通过子类型来实现多态。而在一些场景下,单分派并不能完全解决问题。考虑这样的一个场景:有不同格式的日志,比如 xml, json 或者是 plain text 格式,现在希望能从这日志中抽取一个统一格式的日志。一个简单的实现为:

java
代码解读
复制代码
// 统一 log 类 class UniversalLog{ } // log 接口 interface ILog{ UniversalLog extractUniversalLog(); } class XmlLog implements ILog{ @Override public UniversalLog extractUniversalLog() { return null; } } class JsonLog implements ILog{ @Override public UniversalLog extractUniversalLog() { return null; } } class PlainTextLog implements ILog{ @Override public UniversalLog extractUniversalLog() { return null; } }

通过定义一个 interface 来定义抽取 log 的行为,然后在不同的子类型中去实现具体的抽取方法。这个实现较为简单,逻辑清晰。但是可能面对以下问题:

  1. 如果后续要新增新的抽取逻辑,需要在每个子类型中新增实现
  2. 将行为和数据存放在一起,日志类型也负责了抽取这个行为的实现,违反了单一职能原则
  3. 并不是所有的类都是可控的,比如 XmlLog 完全有可能是第三方库中的类型。

另一个方案是定义一个 LogExtractor 类来完成抽取工作,例如:

java
代码解读
复制代码
class LogExtractor{ public UniversalLog extract(XmlLog log){ return null; } public UniversalLog extract(JsonLog log){ return null; } public UniversalLog extract(PlainTextLog log){ return null; } } LogExtractor logExtractor = new LogExtractor(); logExtractor.extract(new XmlLog()); logExtractor.extract(new JsonLog()); logExtractor.extract(new PlainTextLog());

可以看出,这里使用了方法重载来实现多态来完成抽取逻辑。但是现实场景中,log 的类型往往不能在编译时就决定,因为其往往是从文本解析出来的,因此代码可能是这样:

java
代码解读
复制代码
LogExtractor logExtractor = new LogExtractor(); LogParser logParser = new LogParser(); List fileList = new ArrayList<>(); // some code to visit file system and generate fileList // 从文本中解析日志 List logs = fileList.stream().map(logParser::parse).collect(Collectors.toList()); for(ILog log: logs){ logExtractor.extract(logs); // Cannot resolve method 'extract(List)' }

此时,由于 log 的类型是运行时决定的,因此这种写法在 java 中无法正常工作。其本质是 java 无法支持双分派,无法在运行时通过类型决定调用哪个方法。

访问者模式

为了克服这种缺点,访问者模式就诞生了。既然 java 无法在运行时决定调用哪个方法,那就只能苦一苦开发者,让开发者手动指定。将上述代码进行如下修改:

  1. 将 LogExtractor 修改为泛型,以便后续扩展抽取其他类型数据
  2. 在 Log 类中新增了 accept 方法,用于 显式调用相对应的 extract 方法
java
代码解读
复制代码
interface ILog{ T accept(LogExtractor logExtractor); } class XmlLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extractFromXml(this); // XmlLog 就手动调用 extractFromXml } } class JsonLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extractFromJson(this); } } class PlainTextLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extractFromPlainText(this); } } class LogExtractor{ public T extractFromXml(XmlLog log){ return null; } public T extractFromJson(JsonLog log){ return null; } public T extractFromPlainText(PlainTextLog log){ return null; } } LogExtractor logExtractor = new LogExtractor<>(); LogParser logParser = new LogParser(); List fileList = new ArrayList<>(); // some code to visit file system and generate fileList List logs = fileList.stream().map(logParser::parse).collect(Collectors.toList()); for(ILog log: logs){ log.accept(logExtractor); }

没错,这就是一个访问者模式的实例。上面提到,访问者模型模拟了双分派,那么这个双到底体现在哪里。首先,在代码 log.accept(logExtractor) 会根据 log 实际类型来调用对应子类型的 accept 方法,这里是一次分派。

java
代码解读
复制代码
for(ILog log: logs){ log.accept(logExtractor); }

而在子类的 accept 代码中,手动的调用了 logExtractor 的 extractFromXml。这里是第二次分派,即在运行时根据参数类型来调用对应的方法(虽然是手动的)。

java
代码解读
复制代码
@Override public T accept(LogExtractor logExtractor) { return logExtractor.extractFromXml(this); // XmlLog 就手动调用 extractFromXml }

当然,我们还可以稍微模拟的再像一点,既然 java 支持重载,那么 LogExtractor 完全可以改为:

java
代码解读
复制代码
class LogExtractor{ public T extract(XmlLog log){ return null; } public T extract(JsonLog log){ return null; } public T extract(PlainTextLog log){ return null; } }

而在对应的 accept 方法里面,也可以改为:

java
代码解读
复制代码
class XmlLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extract(this); } } class JsonLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extract(this); } } class PlainTextLog implements ILog{ @Override public T accept(LogExtractor logExtractor) { return logExtractor.extract(this); } }

这样三个方法都可以直接调用 extract 方法,而不是 extractXXX。

总结

由于 java 不支持双分派,因此通过访问者模式来模拟。只不过并不是所有的场景都需要用到双分派。一开始提到的在 Log 子类型中直接实现抽取逻辑在实际的场景中可能反而实现的更多。软件工程没有银弹,我们必须在复杂度和扩展性之间进行平衡。

注:本文转载自juejin.cn的l1n3x的文章"https://juejin.cn/post/7493313134100496420"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top