——“面向对象的三大特性是什么?”
——“封装、继承、多态。”
这大概是最容易回答的面试题了。但是,封装、继承、多态到底是什么?它们在面向对象中起到了什么样的作用呢?
继承
继承(inherit)关系是对象之间的一种层级结构关系。在这个层级结构中,低层级的对象可以获得、也可以修改高层级对象所定义的一些行为和数据。其中,低层级对象“获得”高层级对象的行为和数据的这个动作,也被称为“继承”(做动词用)。
当然,出于封装的考虑,高层级对象也可以把一部分行为“封”起来,不让低层级对象去继承。这并不破坏两类对象之间的继承关系。就好比说,老王的钱终将是小王的钱,但老王的媳妇永远只是老王的媳妇。
Java中有狭义和广义两种继承。狭义的继承就是通过extends关键字实现的类与类之间的继承关系;而广义上讲,通过implements关键字达成的类与接口之间的实现关系也是一种继承:实现类获得了接口中的方法定义,并修改了方法的具体实现。
除了继承之外,对象之间还有一种关系叫组合关系。组合关系是更为普遍、也更为简单的一种关系:类A把类B当做自己的数据(或者叫属性、字段)来使用,我们就说类A组合了类B,或者说是类A依赖于类B。对于只支持单继承的语言来说,继承会形成一个树形结构的类关系网络,而组合则会构建出一个网状结构。
例如,上面这张图的左侧,是一套标准的接口(SomeService类)-模板(SomeServiceAsSkeleton类)-具体实现(SomeServiceDefaultImpl类和SomeService4BizAImp类)、外加一个分发器(SomeServiceAsDispatcher类)的类。这些类按照继承关系组成了一棵树形结构。
而这张图的右侧,是一套与BusinesA有组合关系的类。这这些类按照组合关系组成了一个网状结构。当然,考虑到代码的分层结构(例如上图就是一种business-service/helper-dao的分层结构),在组合关系比较简单的情况下,网状结构也有可能退化成树形结构。但是,就如上图中的ServiceA和HelperA所表达的那样:在组合关系中,它们可以相互依赖;但是在继承关系中,不允许出现相互继承。
继承与面向对象
如果说封装是面向对象的基础,那么继承就是面向对象的核心。有了封装,我们才能构建对象;有了继承,我们才能构建起完整的对象体系。
面向对象思想是一种模拟现实的编程思想。它模拟现实的方式,则是模拟现实世界中的分类体系来构建一套对象体系,用以描述对象是什么、对象能做什么,进而借助这些对象及其能力和关系来实现所需功能。面向对象解决方法的思路、能力以及优势,都是建立在它的对象体系上的。没有这套对象体系,面向对象什么都做不了。
继承就是面向对象用来构建对象体系的方案。在现实世界的分类体系中,任何一个事物都可以用它的上层类别来描述、并拥有上层类别所描述的特性。例如,我是个Java程序员,也是一个程序员、一个IT从业者;《有彩虹的风景》是一幅浪漫主义油画,也是一副画作、一件艺术品……类似的,在面向对象的对象体系中,子类也都可以安全的转为父类。在对象体系中,每一个子类都是一个父类,都拥有父类的数据和能力:ArrayList是一个AbstractList,拥有AbstractList的方法实现;LinkedHashMap是一个HashMap,也是一个AbstractMap,拥有HashMap和AbstractMap的方法实现……
我们可以设想一下,如果没有继承,面向对象思想要怎样构建这个对象体系呢?一个可选的方案是组合。但是,与继承相比,组合是一种更弱、更松散的关联关系。在现实中,老王就可以因为资产贬值而把它卖掉,也可以因为下属犯错而把他炒掉;但是老王不能因为小王闯祸了就甩手不管,因为他们之间的关系非常强、非常紧密。继承比组合更适合于描述这种类之间的“强关系”——也就是我们常说的“is-a”关系。而且,继承关系组成的树状结构比组合关系形成的网状结构也更简单明了、更易于管理和降低复杂度。如果我们不使用继承、而完全使用组合的话,就很容易陷入类爆炸以及类关系网的泥潭中,光是梳理清楚调用关系就已经晕头转向了。
不过,这几个方面的因素只能说是“组合有而继承好”;真正“组合无而继承有”的、让面向对象非使用继承不可的,是继承所带来的抽象能力。
继承与抽象
继承与抽象是同一枚硬币的两面:子类继承父类,那么我们就可以说,父类是子类的一个抽象。我们以下面这张图为例:这是组合责任链模式和模板模式以实现业务功能的一个类图。
先看这张图的上半部分,我们首先可以看到:ProductServiceAsChain和ProductServiceAsSkeleton这两个类都实现了ProductService接口。显然的,ProductService接口是这两个类的一个抽象:它定义了这两个子类“做什么”——从方法名上看,这个接口是要确定使用哪种产品;同时把它们“怎样做“的细节隐藏起来了。
然后我们看图的左半部分,ProductServiceAsFindFirst继承了ProductServiceAsChain,因此我们可以说,ProductServiceAsChain是ProductServiceAsFindFirst的一个抽象:它定义了这一套类要“做什么”——通过遍历serviceList来确定使用哪种产品;而具体“怎样做”才能确定产品,ProductServiceAsChain虽然提供了一个默认实现,但还是隐藏了子类的实现细节。从上图中就可以看到,ProductServiceAsFindFirst通过重写ProductServiceAsChain的方法,提供了另一种方法实现。
最后我们看图的右半部分。ProductService4P1~ProductService4P1都继承了ProductServiceAsSkeleton,所以,ProductServiceAsSkeleton就是这些子类的抽象:它定义了这些子类要“做什么”——先判断应不应该由自己处理,然后再填充产品的具体数据。但是,具体“怎么做”能确定是否由自己处理、“怎么做”才能填充上各产品字段,这些实现细节被ProductServiceAsSkeleton隐藏起来了,只有到具体的子类中才能看到。
从继承的这个角度,可以看到抽象的一个显著特点:它也是分层次的。越是高层级的抽象,其中的细节就越少,对业务的概括能力就越高,可维护和可扩展性也就越好;越是低层级的抽象就越“反其道而行之”:细节信息就越多、业务概括能力越低、可维护和可扩展性也就越差。这也是为什么我们鼓励“面向接口编程”的一个原因。一般来说,接口都是顶层抽象。在它的基础上编程,维护和扩展所受到的限制也就越少。
与“面向对象的三大特性是什么”这个面试题一起出现的,还有“这三个特性中最核心的是什么”。理解了继承与面向对象、与抽象的关系之后,应该就能理解为什么第二道题的答案是“继承”了。
继承与高内聚低耦合
如前所述,继承是一种非常强的关系。因而,借由继承关系构建的模块,其内聚性也非常的高:严格遵守继承规范的情况下,子类和父类只有共同协作才能实现业务功能,二者相辅相成、缺一不可。
例如,我们有这样的一套工厂类:
在上面这套类中,AbstractFactory定义了一套工厂类的模板:首先调用instance()方法创建一个Messager实例,然后通过builderBaseMessager()方法针对Messager的父类属性赋值,最后调用buildDetails()方法对子类属性赋值。其中,instance()和buildeDetails()是两个抽象方法。两个子类分别实现了这两个方法,用以创建不同的对象实例并进行赋值。
在这套工厂类中,如果我们只有AbstractFactory,显然是无法获取正确的实例的:它并不知道我们需要哪个Messager实例,也不知道怎样为这个子类实例赋值。如果只有子类、并且不调用父类方法,我们会遇到类似的问题:虽然能够创建实例,并且能够给它赋值,但是最终得到的实例会缺少一部分数据。
只有父类和子类协作:父类为子类提供基本的框架、子类填补上父类留下的空白,才能得到完整的、正确的结果。而这正是内聚性最高的“功能内聚”。
Functional cohesion is when parts of a module are grouped because they all contribute to a single well-defined task of the module .
功能内聚是指一个模块内所有组件共同完成一个功能、缺一不可。
花园的景昕,公众号:景昕的花园《细说几种内聚》
但是在开发实践中,大多数的父类就可以“独当一面”完成功能了;子类所做的并不是“完成未竟的事业”,而是“偷梁换柱”地用另一种方式完成业务功能:似乎父子类之间也并非没你不行啊。
这是我们在遵守继承规范和提高开发效率这一对“原则性”和“灵活性”之间进行取舍的结果。使用继承的规范相当的“严苛”。严格遵守规范会增加开发的工作量、会提高类结构的复杂度、会增加类的个数,最终会降低开发效率、影响项目排期。
与超高的聚合性类似,继承关系的类之间耦合度也非常强。人们甚至造了一个专有名词来描述父子类之间的这种超强耦合:子类耦合(subclass coupling)。
Describes the relationship between a child and its parent. The child is connected to its parent, but the parent is not connected to the child.
子类耦合描述的是子类与父类之间的关系:子类链接到父类,但是父类并没有链接到子类。
花园的景昕,公众号:景昕的花园《细说几种耦合》
子类耦合在《细说几种耦合》一文中已有讨论,所以这里就不多赘述了。
继承与其它
“is-a”与“like-a”
由于继承会带来超高的耦合性,所以很多时候我们都建议尽量少用继承——尤其是狭义的继承,即extends。我们听得最多的,就是只有“is-a”关系才可以使用继承;如果只是“like-a”关系,那么应该使用组合。
但是,怎样算“is-a”、怎样算“like-a”呢?在我看来,如果两个类在“做什么”方面如出一辙、只是在“怎么做”上大同小异,那么它们就是“is-a”的关系;如果他们在“做什么”上大相径庭、而只是在“怎么做”上有些异曲同工,那么它们就是“like-a”的关系。
例如,在下图中,在我们的系统已有一个类ApplyServiceImpl可以对身份证号做基本校验了。随着业务发展,我们有两个新的类(暂时化名为NewServiceA和NewServiceA,否则看到类名就能猜到答案了)需要复用并扩展ApplyServiceImpl的身份证校验逻辑:在基本校验之外,它们都需要增加ID5认证。那么,这两个新的类与原先的那个类之间,是“is-a”还是"like-a“呢?
虽然隐去了类名,但是从两个新类所提供的方法上,我们还是可以看出些端倪来。NewServiceA和ApplyServiceImpl的主方法是一样的:apply(User):Apply,这说明它们所做的是同一件事情,只是在做这件事情的过程中有一些差异。而NewServiceB和它俩做的几乎完全是两码事,只是在个别细节上恰好“英雄所见略同”。所以,这三个类之间的关系可以说是一目了然的, 最后的类结构则是这样的:
面向对象思想中还有一句名言,可以用做“like-a”与“is-a”的参考:“如果它看上去像只(like-a)鸭子,听上去像只(like-a)鸭子,飞起来也像只(like-a)鸭子,那么它就是只(is-a)鸭子”。也就是说,只有“全方位”的“like-a”的时候,才可以转为“is-a”关系;否则,还是慎用“is-a”这种强关系。
类与接口
细心看了上文的话,就会发现,在ApplyServiceImpl-ApplyService4ProductBImpl-BankCardServiceImpl最后的类图中,多了一个新增的接口:IdCardService。
但是,我们为什么一定要新增一个接口呢?按前文所述,ApplyServiceImpl同样是ApplyService4ProductBImpl的一个抽象,为什么BankCardServiceImpl一定要使用新的接口IdCardService、而不直接使用类BankCardServiceImpl呢?换句话说,为什么一定要面向接口编程呢?
细心看了上文的话,就会发现,其实前面已经回答过这个问题了。
首先,抽象“也是分层次的。……一般来说,接口都是顶层抽象。在它的基础上编程,维护和扩展所受到的限制也就越少”。而类虽然也有一定的抽象能力,但是其中已经包含了相当多的细节,不如接口那么富有“生命力”。
例如,使用Map<Enum, String>接口时,我们既可以用HashMap<Enum,String>,也可以用EnumMap<Enum>、ConcurrentHashMap<Enum, String>。但如果一开始就使用了HashMap,那么后续想要变更实现类时,可能就举步维艰了。我们有位同事曾经在代码中用HashMap来封装数据、并把它在Controller、Service、Dao中层层传递和扩散开来。然而,在code review中我们发现这个map存在并发风险,需要替换成ConcurrentHashMap。此时我们才发现,这一整套代码都跟HashMap绑定了,就像船只被章鱼海怪死死缠住一样,进退维谷,左右为难。
其次,面向对象思想通过继承“构建一套对象体系,用以描述对象是什么、对象能做什么”。具体到类和接口上来说:类同时描述了它是什么和它能做什么;而接口仅仅描述了它能做什么。在代码中,类A调用类B时,其实A并不关心B是什么,而只关心它能做什么——如果你养宠物的目标只是捉住老鼠,那就不要管养的是黑猫白猫、甚至是狼是狗了。反过来想想,如果类A在调用类B时,不光要求它“能做什么”,还限定了它必须“是什么”,那么在后续的功能扩展中就难免受到限制。设想一下,如果你养宠物时,不仅要求能捉住老鼠,还限定它必须是猫,那么当遇上吃猫鼠时,就“勿谓言之不预”了。
对于“类同时描述了它是什么和它能做什么;而接口仅仅描述了它能做什么”,我们还可以这样理解。在继承结构中,类是给它的子类看的:子类需要从父类定义中知道我们“是什么”、我们“能做什么”;并在逐层细化、重写与扩展中找到我“是什么”、我“能做什么”的定位。接口的定义是给外部调用者看的:只需要告诉它们自己“能做什么”,至于自己“是什么”,“不足为外人道也”。
![](https://img.haomeiwen.com/i11527915/bf336b8f1a60f420.png)
网友评论