泛型和继承是现代编程语言中两种比较重要的特性,对提高语言的表达能力,增强软件的质量、健壮性、可维护性有重要作用。前者常见于函数式编程语言,如Haskell;后者则是面向对象(OO)语言的基础。泛型对类型的描述更细化,表达能力更强,然而,泛型是编译期的信息,无法提供像继承中的动态绑定功能,这也许是过去二十年中OO语言得到广泛使用的原因。
之所以说泛型不能实现动态的效果,主要原因在于:
1) 泛型信息仅仅在编译期起作用
2) 泛型不支持类型的向下转换(Down Casting),即子类对象转换成作为父类型使用
这使得泛型在实现类似虚函数的多态时,无法实现或者极为麻烦。考虑如下的情况
class Animal:
method eat ...
class Cat inherite Animal:
method eat ...
class Dog inherite Animal:
method eat ...
这在OO中是很常见、很基本的。然而如果用泛型实现,则很麻烦。比如用Pattern Matching
func eat animal:
Cat cat = ...
Dog dog = ...
调用时
eat(cat, ...)
eat(dog, ...)
似乎也可以。但是,如果cat
或dog
是由某个Factory根据配置文件动态产生的,也就是
cat = Factory.create_animal ...
现在create_animal
的返回值类型如何写?显然,由于泛型中不能将Cat
和Dog
都转换成父类Animal
来处理,就无法在运行时依据animal的实际类型,调用对应的函数;而必须在编译期确定所有的类型和应当调用的函数。
无法进行动态绑定,也就无法进行软件的动态扩展。比如,在Java中,上述代码已经打包成Jar包,现在要增加一个类型Pig,我们只需让Pig继承Animal,将新代码打成Jar包即可使用,无需对原有的代码进行改动和编译。而对于泛型的版本而言,不但无法实现动态扩展,而且还要修改原始的eat函数,加入Pig对应的Pattern代码。
这一特性使得OO语言能够很好的支持“开闭”原则,即代码对扩展开放,对修改封闭。通常人们认为,OO的优势在于对现实世界中“对象”的模拟,但是我更认同松本行弘的观点,即:如同结构化编程一样,OO是一种代码组织的方式,使得软件开发中的复杂度能够得到更好的控制,至于它是否模拟了世界,并不重要。
在我开来,OO的作用是把接口和实现分离,并且将实现的函数体拆分到了多处。在上述例子中,增加了Pig类型,实际上是在eat函数中增加了功能,能够处理Pig类型的变量。从另一个角度说,多态的eat函数等价于
eat animal, ...:
if animal instance of Cat:
...
elif animal instance of Dog:
...
加入了Pig类型,等于增加了一个if分支:
elif animal instance of Pig:
...
而这种增加,既没有改变接口,也不需要修改原来的代码,而是通过类的继承。所以,通过类型继承实现的多态,在不改变接口的前提下,把实现函数的函数体根据具体类型拆分到了多处,并且可以增加新的部分,而不影响原有部分。这显然就是对“开闭”原则的实践。
“开闭”原则对提高软件的可扩展性,控制复杂性有重要作用,在大型软件开发中尤为明显。过去20年间,C++、Java等OO语言获得了广泛使用。而Haskell、Lisp、Erlang等语言尽管更清晰、简洁,有更好的数学基础,或者并发的效率更高,但是却没有获得广泛的普及。仅仅把原因归咎于曲高和寡是不够的。在作者看来,这些函数式语言因为无法提供类型继承和动态绑定的功能,导致不能很好的支持开闭原则,在大型软件开发中不能很好的控制复杂度,是它们没有获得广泛应用的主要原因,尤其是在应对需求复杂多变的场合方面。