./Mutiple Inheritance

posted by cli on

Mutiple Inheritance

继承是大多数面向对象语言中很重要的一个概念,继承一般分为单一继承和多重继承。由于多重继承固有的问题,多数人对多重继承有一个误解,认为多重继承是不好的。希望本文能够阐明一些对多重继承的误解。

引言

绝大多数面向对象语言有三大共同的原则:多态性、数据抽象和继承。继承这个概念来自于1968的Simula语言,Simula是第一个面向对象的高级语言,继承自诞生起就一直在面向对象编程语言中扮演着重要的角色,以至于绝大多数面向对象语言都构筑在继承的基础之上。

从面向对象编程的角度看继承,继承主要是为了实现对代码的重用以及建立对象间的层次关系。正因为如此,继承这个概念在基于类的面向对象语言中显的尤为重要。在实际使用时,继承的主要问题是,复杂的继承关系或者在不成熟的设计中使用继承会导致问题。很长很复杂的继承关系使得阅读代码的人很难理解程序的整体结构,这个问题通常称为yo-yo问题;另一方面,如果语言允许多重继承,在不良设计的继承关系中会出现二义性问题,二义性会导致优先顺序模糊以及功能冲突。

继承是随着程序的结构化和抽象化自然进化而来的一种方式,结构化和抽象化意味着把共通的部分提取出来生成父类的自底向上的方法。如果系统中的继承关系是这么确定的话,多重继承是很自然的想法。但是纵观历史,最初引入继承的Simula语言只提供单一继承,在随后的很多面向对象语言中也都是这样。继承的原本目的应该是逐步细化,是一个自顶向下的过程。

为什么需要多重继承

单一继承只能有一个父类,有时候这样的约束过于严格了。在现实中,一个老师同时可能是一位父亲,一个程序员同时可能是一位音乐家。这种情况下单一继承在编程中会带来很大的不便,多重继承的思想就是这么诞生的。本质上说,多重继承和单一继承仅仅是父类的数量不同,多重继承是单一继承的超集。

考虑实现输入输出的Stream类,Stream类有三个子类。其中,ReadStream是输入类;WriteStream是输出类;ReadWriteStream是输入输出类,具有ReadStream和WriteStream两个类的功能。在只支持单一继承的语言中,ReadWriteStream不能同时继承两个类,必然会导致重复的代码。从另外一个角度看,如果语言支持多重继承,ReadWriteStream可以很自然的从ReadStream和WriteStream类继承。

单一继承的特点是继承关系单纯,单一继承的继承关系是树状结构,而多重继承的类之间是网状关系。正因为如此,多重继承在一部分人当中的评价并不好。但是在某些语言中,考虑到生产力时多重继承是必需的。单一继承有利有弊,好处是类之间的关系不会发生混乱,坏处是不能通过继承关系来重用共通代码,这在某些面向对象的语言中是个很大的制约。多重继承的特点刚好相反,但是有两个优点:很自然的扩展了单一继承;可以继承多个类的功能。缺点是类之间的关系会变得复杂,使用时必须格外小心。

如何解决多重继承的问题

既想利用多重继承的优点,又要回避它可能带来的固有复杂性,就需要寻找解决问题的方法。结构化编程解决goto问题的原则是:用三种有功能限制的控制语句(顺序、选择和循环)来代替原来功能更强大的goto语句。这三种控制语句虽然有限制,但是用他们的组合可以实现与goto等价的功能。事实上,多重继承的问题也可以通过引入限制解决。

例如,Java中的接口就是受限的多重继承,接口只是规格的继承。由于静态语言的关系,Java中实现继承和规格继承的区别很重要。Java对两者有很明确的区分,实现继承用extends,规格继承用implements,同时接口仅仅是用来定义对象的外观。Java只允许用extends继承一个父类,但是可以implements多个接口,接口对实现没有任何限制。以上可以说就是Java对多重继承问题的解答,既实现了静态语言的多重继承,又避免了多重继承中类层次的复杂性。但是,接口并不是解决多重继承问题的完美方案,接口存在不能共用实现的问题。针对重用问题,Java推荐的方案是,在单一继承的前提下,使用组合模式用专门的类来实现共通功能。为了共享代码,还要另外生成一个独立对象,而且每次方法调用都要转发给那个对象,且不提效率问题,这种方式本身就很不合理。

和静态语言不同,动态语言本来就没有规格继承这种概念,因为即使没有继承关系也可以自由地调用方法,所以动态语言需要解决的是实现的多重继承。因此很多动态语言支持一种称为Mixin的机制。Mixin类似于虚基类,最大的不同点在于Mixin是通过包含而不是继承来实现代码重用。Mixin可以看成一种多重继承,但是Mixin的类或对象之间并不存在层次关系。Mixin降低多重继承的复杂性,实现Mixin并不需要编程语言提供特殊的功能。和接口不同,Mixin对多重继承的限制要少一些,具体说来:通常的继承用单一继承;第二个以及两个以上的父类必须是Mixin的类。一个Mixin类具有以下特征:不能单独生成实例;不能继承普通类。按照这些原则,类的层次结构依然是单一继承的树状结构,同时又可以实现共通部分的代码重用。共通部分的实现放在Mixin类中,将Mixin类插入到类的树状结构中。相对于Java使用接口方法来解决规格继承问题,Mixin可以说是较好的解决了实现继承问题。另一方面,和多重继承相比,Mixin使得类的层次结构变得简单容易理解。

另外需要说明的是,Mixin的具体实现并不依赖类形式的对象系统。像JavaScript、Common Lisp和Clojure这些语言都支持Mixin,但他们的实现不是基于类的。

另一个和Mixin非常类似的技术是Trait,简单的说Trait是一组用来实现类行为的可参数化方法。Mixin最大的问题是其组合只能是正交的Mixin,Mixin并没有解决多重继承各个基类间功能冲突的问题。另一方面,Mixin本身对继承层次有影响,修改Mixin可能会使原本的继承关系出现问题,这导致了类之间相当脆弱的层次关系。Trait的思想是,类通过胶连代码用各自独有的状态去参数化Trait提供的行为,以此来做到代码的重用。Trait定义了明确的解决冲突的方式,以及更加灵活的方法选择方式。我没有使用过Trait,不便在此详述,有兴趣的读者可以自行查阅资料。

总结

关于继承多数人最大的误解是认为多重继承是不好的,但是这个观点是错误的。从软件工程的角度而言不受限的多重继承确实不好的,会导致系统设计复杂化。但是纵观各种编程语言,从来就不缺多重继承的身影。从实际情况出发,我们需要多重继承提供的便利,也想尽量避免多重继承带来的各种潜在问题。很多面向对象的编程语言都有各自尝试,接口、Mixin以及比较新的trait,都是编程语言领域研究者们的各种尝试。关于多重继承,正确的理解应该是,如果用的不好就会出现问题,这和goto语句是非常类似的。对接口、Mixin以及trait的正确理解应该是,它们都是实现多重继承的技巧而已。

多重继承并不可怕,在绝大多数面向对象编程语言中继承是一个很重要的组成部分,而从实用角度出发,多重继承是面向对象语言中必不可少的功能,所以理想的面向对象语言必须以某种方式支持多重继承。从本质上看,类既有类型的一面,也有模块的一面。类型是行为的抽象,而模块则是代码重用的基础。我们需要解决的问题正是如何给多重继承施加合适的约束,让它能够更好的服务于行为的抽象和代码重用,因为这两点才是面向对象语言中继承的目的。

正确使用多重继承是提高大多数面向对象语言生产效率的有效方法,本文的目的是阐明多重继承对面向对象语言的意义,减少人们对多重继承的误解。