OOP 三大特征之多态(Polymorphism)

x33g5p2x  于2021-10-10 转载在 其他  
字(2.5k)|赞(0)|评价(0)|浏览(366)

OOP三大特性最重要的:多态。

很多程序员虽然在用支持OOP的语言,但却从未用过多态。

  • 只使用封装、继承的编程方式,称为基于对象(Object Based)编程
  • 只有加入多态,才能称为OOP
    没写过多态,就是没写过OO代码。

正是有了多态,软件设计才有更大弹性,更好拥抱变化。

如何理解多态?

多态,即一个接口,多种形态。

一个draw方法,以正方形调用,则画正方形;以圆形调用,则画圆形:

继承的两种方式之一的实现继承,请尽可能用组合替代。而接口继承,主要是给多态用的。

因为重点在于继承体系的使用者,主要考虑父类,而非子类。
如下代码段,不必考虑具体形状是啥,仅需调用它的draw方法

优势在于,一旦有新变化,比如将正方形换成圆,除了变量初始化,其它代码不需要动。
既然多态这么好,为什么很多人感觉无法在项目中自如地多态?

多态需构建抽象。

构建抽象

找出不同事物的共同点,这是最具挑战的。令人懵逼的也往往是眼中的不同之处。在很多人眼里,鸡就是鸡,鸭就是鸭。

寻找共同点,根基还是分离关注点
当你能看出鸡、鸭都有羽毛,都养在家里,你才可能识别“家禽”。
构建出的抽象会以接口(此处接口不一定是个语法,而是一个类型的约束)体现。所以,本文讨论的多态范畴内,接口、抽象类、父类等概念等价,统一称为接口。

接口的意义

接口隔离了变化部分、不变部分

  • 不变部分
    接口的约定
  • 变化部分
    子类各自的实现

最影响程序的就是各种变化。有时需求来了,你的代码就得跟着改,一个可能的原因就是各种代码混在了一起。
比如,一个通信协议的调整,你要改业务逻辑,这明显不合理。
所以识别出变化与不变,是区分程序员水平的一大标准。

接口是边界

清晰界定系统内不同模块的职责很关键,而模块间彼此通信最重要的就是通信协议,对应到代码中的接口。

很多程序员在接口中添加方法很随意,因为他们眼里,不存在实现者和使用者的角色差异,导致没有清晰边界,后果就是模块定义随意,彼此之间互相耦合,最终玩死自己。

所以,理解多态在于理解接口,理解接口在于谨慎选择接口中的方法。

面向接口编程的价值就源于多态。

这些原则你可能都听说过,但写代码时,就会忽略细节。
比如:

这显然没有面向接口编程,推荐写法:

差别就在于变量类型,是面向一个接口,还是面向一个具体实现类。

多态对程序员的要求更高,需要你能感知未来变化!

实现多态

OOP会限制使用函数指针,它是对程序控制权的间接转移施加了约束。
理解这句话,就要理解多态如何实现的。

Linux文件系统用C实现了OOP,就是用了函数指针:

即可这样赋值:

给该结构体赋不同值,就能实现不同文件系统。
但这样非常不安全。既然是个结构体字段,就可能改写它:

本该在hellofs_read运行的代码,跑进了sillyfs_read,程序崩溃。对于C这种灵活语言,你无法禁止这种操作,只能靠人为规定和代码检查。

到了OOP 语言,这种做法由一种编程结构变成一种语法。给函数指针赋值的操作下沉到了运行时去实现。运行时的实现,就是个查表过程:

一个类在编译时,会给其中的函数在虚拟函数表中找个位置,把函数指针地址写进去,不同子类对应不同虚拟表。
当用接口去调用对应函数时,实际上完成的就是在对应虚拟函数表的一个偏移,不管现在面对哪个子类,都可找到相应实现函数。

C++这种注重运行时消耗的语言:

  • 只有virtual函数会出现在虚拟函数表
  • 普通函数就是直接的函数调用,以此减少消耗

对于Java程序员,可通过给无需改写的方法添加final帮助运行时优化。

当多态成为语法,就限制了函数指针的使用,犯错率大大降低!

没有继承的多态

封装,多态。至于继承,却不是必然选项。只要能够遵循相同接口,即可表现出多态,所以,多态并不一定要依赖继承。

动态语言中一个常见说法 - Duck Typing,若走起来像鸭子,叫起来像鸭子,那它就是鸭子。
两个类可不在同一继承体系下,但只要有相同接口,就是一种多态。

如下代码段:Duck和FakeDuck不在一棵继承树上,但make_quack调用时,它们俩都可传进去。

很多软件都有插件能力,而插件结构本身就是多态。
比如,著名的开源图形处理软件GIMP,它自身是用C开发的,为它编写插件就需要按照它规定的结构去编写代码:

struct GimpPlugInInfo
{
  /* GIMP 应用初始启动时调用 */
  GimpInitProc  init_proc;

  /* GIMP 应用退出时调用 */
  GimpQuitProc  quit_proc;

  /* GIMP 查询插件能力时调用 */
  GimpQueryProc query_proc;

  /* 插件安装之后,开始运行时调用*/
  GimpRunProc   run_proc;
};

我们所需做的就是按照这个结构声明出PLUG_IN_INFO,这是隐藏的名字,将插件的能力注册给GIMP这个应用:

GimpPlugInInfo PLUG_IN_INFO = {
  init,
  quit,
  query,
  run
};

这里用的C语言,但依然能表现多态。

多态依赖于继承,这只是某些程序设计语言自身的特点。在面向对象本身的体系中,封装和多态才是重中之重,而继承则很尴尬。

一定要跳出单一语言的局限,这样,才能对各种编程思想有更本质的认识。

OOP三大特点的地位:

  • 封装是面向对象的根基,软件就是靠各种封装好的对象逐步组合出来的
  • 继承给了继承体系内的所有对象一个约束,让它们有了统一的行为
  • 多态让整个体系能够更好地应对未来的变化。

FAQ

某系统需要对普通用户增删改查,后来加了超级管理员用户也需要增删改查。把用户的操作抽象成接口方法,让普通用户和管理员用户实现接口方法…… 那么问题来了,这些接口方法的出入参没法完全共用,比如查询用户信息接口,普通用户和超级管理员用户的返回体信息字段不同。所以没法抽象,请问一下老师这种应不应该抽象呢?如果应该做成抽象需要怎么分离变的部分呢

应该分,因为管理员和普通用户的关注点是不同的。管理员和普通用户可以分别提供接口,分别提供相应的内容。
如果说非要二者共用,可以考虑在服务层共用,在接口层面分开,在接口层去适配不同的接口。

总结

多态是基于对象和面向对象的分水岭。多态就是接口一样,实现不同。
建立起恰当抽象,面向接口编程。

相关文章