面试经常会被问到的题目之一,面向对象的三大特征是什么?多态则是三大特征之一,个人认为三大特征中最为重要的,另外的两大特征是封装和继承。
为什么说多态对软件架构师非常重要,对系统软件非常重要呢?举个例子,当软件面向一个客户的时候,你会发现软件写得很简单,很快就能满足其需求。随着时间的推移,软件面向的不再是一个客户。每个客户提出的需求千差万别,尤其当出现针对性的、个性化的需求。软件的迭代、升级会变得相对困难,拓展功能变得困难。
然而,多态很好的帮助我们解决该问题。多态对源代码的依赖关系具有很好的控制能力,这种能力让软件架构师可以构建出插件式架构,让高层策略性组件与底层实现性组件分离,底层组件可以被编译成插件,实现独立于高层组建的开发和部署。
也就是说当我们很好的运用多态,有三大优势:
1、控制源代码的依赖关系;
2、组件独立部署能力;
3、组件独立开发能力;
Robert C. Martin 在《整洁架构之道》中很好的阐述了多态的强大性,今天我们来学习一下。
以下:
依赖反转
我们可以想象一下在安全和便利的多态支持出现之前,软件是什么样子的。下面有一个典型的调用树的例子,main 函数调用了一些高层函数,这些高层函数又调用了一些中层函数,这些中层函数又继续调用了一些底层函数。在这里,源代码层面的依赖不可避免地要跟随程序的控制流。
图:源代码依赖与控制流的区别
如你所见,main 函数为了调用高层函数,它就必须能够看到这个函数所在的模块。在 C 中,我们会通过 #include 来实现,在Java 中则通过 import 来实现。而在 C#中则用的是 using 语句。总之,每个函数的调用都必须要引用被调用放所在的模块。
显然,这样做就导致了我们在软件架构上别无选择。在这里,系统行为决定了控制流,而控制流则决定了源代码依赖关系。
当我们使用了多态,情况就不一样了。
图:依赖反转
如你所见,模块 HL1 调用 ML1 模块中的 F() 函数,这里的调用是通过源代码级别的接口来实现的。当函数在程序实际运行时,接口这个概念是不存在的,HL1 会调用 ML1 的 F() 函数。
模块 ML1 和接口 I 在源代码上的依赖关系或者叫继承关系,该关系的方向和控制流正好是相反的,这就是依赖反转。这种反转对软件架构设计的影响非常大。
事实上,通过利用面向对象编程语言所提供的这种安全便利的多态实现,无论我们面对怎样的源代码级别的依赖关系,都可以将其反转。
通过这种方法,软件架构可以完全控制采用了面向对象这种编程方式的系统中所有的源代码依赖关系,而不再受到系统控制流的限制。不管哪个模块调用或者被调用,软件架构师都可以随意更改源代码依赖关系。
接下来我们看一个案例:典型的系统分为用户层、业务层、数据库。通常我们的依赖关系是用户层 Controller 调用业务的 Service,业务层调用数据库层 DAO 或者资源库 Repository。
图:数据库和用户界面都依赖于业务逻辑
这意味着我们让用户界面和数据库都成为业务逻辑的插件。也就是说,业务逻辑模块的源代码不需要引入用户界面和数据库这两块。
这样一来,业务逻辑、用户界面以及数据库就可以编译成三个被独立的组件或者部署单元(例如 jar 文件、DLL 文件、Gen 文件等),这些组件或者部署单元的依赖关系与源代码的依赖关系是一致的,业务逻辑组件也不会依赖于用户界面和数据库这两个组件。
于是,业务逻辑组件就可以独立于用户界面和数据库来进行部署了,我们对用户界面或者数据库的修改将不会对业务逻辑产生任何影响,这些组件都可以被分别 独立部署。
简单来说,当某个组件的源代码需要修改时,仅仅需要重新部署该组件,不需要更改其他组件,这就是独立部署的能力。
如果系统中的所有组件都可以独立部署,那它们就可以由不同的团队并行开发。这就是所谓的独立开发能力。
总结
看似简单,实则大部分程序员并没将其运用到各自到系统软件中。前段时间微服务很火、DDD也很热,很多思想都离不开多态。系统软件存在的意义是解决用户问题,用户问题则是核心部分,也就是用户业务逻辑。如何有效的分离系统软件的业务逻辑,控制与业务逻辑无关的代码,从而降低软件复杂度是一名优秀的工程师或者架构师必经之路。