第3. 一切皆对象和面向对象的理论基础
老庄是反对一切皆对象的,而TrustNo1在javaeye的一篇帖子上说:
我先说面向对象的理论基础的问题,至于一切皆对象稍后再表。所谓面向对象的理论基础其实是没有的,原因很简单,面向对象根本就不是一种计算模型。在第一次软件危机的那个时代,对与计算机的非数值计算应用的讨论以及对于可计算性问题的研究和发展,大抵确立了几种主流的计算模型:递归函数类,图灵机,Lambda演算,Horn子句,Post系统等等。其中递归函数类是可计算性问题的数学解释;图灵机是图灵解决可计算问题的时候所设计的装置,其后成为计算机的装置模型,与图灵机相关的形式语言和自动机成为了命令式语言的理论基础;lambda演算成为了函数式语言的理论基础;Horn子句是prolog这类逻辑语言的理论基础。但是我们惊讶的发现,面向对象没有计算模型的理论基础,换而言之,面向对象根本就不是从可计算性的研究上发展过来的,那么面向对象的理论基础的价值本身就不大。
所以我很奇怪的一个问题就是TrustNo1所谓的面向对象在80年代理论基础上给人毙掉的说法是从何而来的?既然面向对象本质上不是一种计算模型,那么它大抵上只能归结为一种应用技术,应用技术自然可以从各种不同的领域里得到相似的应用,那么毙掉的理论基础所指的又是什么呢?甚怪之。
既然面向对象不是一个计算模型,那么我们可以从不同的角度推断出OO的各种形态,老庄已经出给了从ADT引出OO的问题以及例子,我就不罗嗦了,我给一个从Functional Programming出来的例子,其实就是SICP里的Data as Procedure。
然后我们就可以
自然的,如果我调用
会得到一个messageNotUnderstand的runtime错误,这就是一个很自然dyanmic type的对象封装,最早的面向对象系统Smalltalk和CLOS基本上都是这个路子,于是有一个问题,为什么最早的面向对象系统都是dyanmic type?这里就跟lambda演算有关了。
lambda演算这个计算模型根本的一个假设就是,对于任何一个定义良好的数学函数,我都可以使用lambda抽象来表述他的求值,因此无论是什么东西你能够构造lambda抽象的话,我就能计算。这个地方东西很多,大家可以找找lambda演算相关的资料,这里我说三件事(由于lambda太难输入,我用scheme程序代替,然后由于alpha变化,beta规约和eta规约我也用scheme伪码来模拟。)
第一个是数值的表述,其实这个很简单,不考虑丘奇代数的系统的话,我们可以把数值表示成单值函数:
这个东西无论给什么x都返回1,然后根据lambda演算里的alpha变换,这个lambda抽象等价于数值1。因此,对于所有的数值,我们可以按lambda演算处理。
第二个是bool的表达,也就是如何逻辑进行lambda抽象,下面我直接给出了,缺省认为大家都看了SICP,对Scheme也颇有心得。
然后我就可以做一个测试
因此,对于所有bool我们可以按lambda演算来处理
第三个是自定义类型,这里我们还是先看一个Lisp里的例子,序对。
这里依旧是high-order,我们来测试
这里大家自己用beta规约算一下,就发现的确是这样的。这里我们又可以看到,在lambda演算里,根本没有数据或者类型。有的永远各种各样的lambda抽象而已(目前已经有了带类型的lambda演算)。这里说一句题外话,SICP里的Data as Procedure其实就是在说这个问题,不过他没明说,而是用了一种特殊的用法而引出了消息传递风格,我觉得这里SICP有些顾此失彼,对于data as procedure我总结的一般形式是
综上所述,我们看到在lambda演算里,一切都是lambda抽象,然后对于不同的lambda抽象使用alpha变换,beta规约和eta规约,表述各种不同计算。看,在面向对象之前就已经有了一切皆某某的完美的计算理论存在了。而且回顾一下:
我们有理由说,对象其实就是一个lambda抽象,所以一切皆对象不过是一切皆lambda抽象的演化,这也是为什么SICP里把面向对象称作一种“方便的界面”而不是一种抽象的方法的原因了。那么对象和lambda抽象又什么区别呢?嘿嘿,熟悉FP的人都应该知道了,就是Side-Effect,副作用。对象允许对其内部状态进行修改,那么这个东西就破化了eta规约的前提条件,也就是说允许修改内部状态的东西,已经不是一个可以进行lambda计算的lambda抽象了。因此暂且给一个别的名字吧,就叫对象吧.....因此我认为,对象很大程度上是一种带有副作用的lambda抽象。
我在有一篇blog上写了Say sorry to object-oriented,里面给了一只用对象作分支的例子,这里就不重复了,有兴趣大家可以去看一下(刚才好像在JavaEye上看到一个说法,说Everything is Object是Smalltalk的广告语,唉,Smalltalk里的的的确确everything is object啊。)这里我们来总结一下,面向对象作为一种“方便的界面”主要解决了一个局部化和模块化的问题,这是从lambda演算和函数编程的角度来看面向对象技术。(taowen在他的一篇blog上说,面向对象局部化了Side-Effect,我深以为然),这个东西我个人认为更加接近面向对象本来的意思,而不是由ADT里发展出来的带类型的对象系统的那个意思。因此老庄不以为然的面向对象类型系统,我也不以为然。但是面向对象作为lambda抽象的界面,我觉得还是很不错的。这一点在Smalltalk和Ruby里都有不错的体现。
第2 继承
前面已经说过了,继承至少有2个语义: 实现继承和类型继承,在说明这两个东西之前,我们继续来看上面的例子
现在我们看playBWV996这个消息,BWV996是Bach所写一个鲁特琴组曲,鲁特琴是弹拨乐器,同时也是和声乐器(所谓和声乐器就是可以演奏和声)。在很多乐器上都有改编的版本,比如鲁特琴的近亲吉他等等,这个时候,我们可以实现这样几个类
然后我们可以尝试以此调用
最后一个会得到一个messageNotUnderstand的错误。也就是说,对于Bass而言由于不能演奏和声从而不能演奏BMV996(不过这个世界上能人太多了...哎),我们换到静态类型面向对象系统来看。对于第一个方法,playSolo的时候我们要求的类型是能够演奏单音的。我们可以写出来
对于第二个方法,playChord的时候我们要求的类型是能够演奏和弦的,我们可以写出来
而对于第三个方法,playBWV996的时候我们要求既能演奏和弦也能演奏单音,这个时候出现一个问题,我们怎么处理Instrument的继承关系?一个能演奏和弦的乐器是否可以演奏单音(答案是一般而言是的,但是也不排除有一些不是这样的)?还是我们简单的写:
或者
对于动态类型简单的隐性类型约定,显示的类型系统带来的一个副作用就是我们必须处理类型之间的关系。注意这里是类型之间的关系,而不是对象之间的关系。老庄同志批了很多篇的面向对象的抽象,面向对象的类型系统以及面向对象的本体论,其实都在是在类型关系上折腾,而不是在对象关系上折腾。而事实上面向对象的类型系统并非必然就是静态类型系统,而我们的类之间的关系不一定就和类型的关系相一致。就像上例所示,在Smalltalk里,Lute,Guitar和Bass之间没有任何的继承关系,但是对于person的3个消息而言,它们却是有类型的。因此老庄所批的,是对象类型系统的抽象能力,而非面向对象的抽象能力。正如他在类型系统里所给的例子,那张他认为很失败的面向对象的图,其实可以完全不依赖继承来实现,而对这个类型系统的消费者而言,他们能够以一定的类型的观点,来处理这个系统的对象。
而老庄最后一个结论:
我的看法是,这句话根本就是诡辩,前面半句的主语是“一个类型”,后面半句的主语是"OO"...虽然前半句是对的,但是换一样说法可能更好:"所能接受的操作反映了其本质",面向对象本身就没有说我要做一个本质抽象,这一点在Smalltalk的类型判断操作上的可能是一个佐证,Smalltalk用isKindOf来判断继承关系,我们来玩一个文字游戏,改成俚语就是kinda,也就是"有一点,有几分"的意思,而不是说,“就是”,或者“从分类学上可证明之类的含义”。我再举一个龌龊的例子。
气球和保险套,对于ballon这个方法而言是一个类型,都是"有几分"可以吹起来。但是我怎么定义一个精确的本质?Ballonable?还是MakeFromLatexAndVeryThin?或者简单说FlexableAndThin?
在继承这一点上,我想老庄引文中:Elminster的话是从事物的特征与属性归纳出它的“类型”。恰恰对于静态类型面向对象系统是可行的。如我前文所述,我把一个object和所有sender的约定(也就是interface),继承在一起,恰恰就是一个颇为恰当的类型定义。而对于动态类型系统里的面向对象语言,继承的也有类型继承的含义,但是并不是唯一的途径。用一句我们常说的话,在静态类型系统里,类型和类是紧耦合的,动态类型系统中他们的耦合比较松。从此而观,所有对于面向对象的哲学考虑以及本体的思考,对于动态面向对象系统已经不是那么迫切了。而把对象类型系统的不足归咎于面向对象的不足,也似乎论据不足。
第1 关于"接口"
关于接口的问题老庄说对了,这个东西并不属于面向对象的概念,而且在动态类型面向对象语言(比如Ruby, Smalltalk里),根本没有这个东西。这是一个纯粹的静态类型面向对象语言的特性,或者直接说,接口就是一个纯类型(Type)。还是上次的例子:
在我接触Smalltalk的最开始的一段时间里,这种地方是让我最难受,我已经习惯了用类型辅助我的思维,但是我发现我在Smalltalk里做不到,虽然我可以写出
但是他却不是一个接口,我可以很用
来构造一个Instrument之后在它之上调用playNote方法,然而我会得到一个messageNotUnderstand的错误,Smalltalk和Ruby里没有Abstract的概念。也就是说abstract method,abstract class以及interface,都不是面向对象的概念(或者严格一些说,都不是面向对象的必须的概念),而是面向对象类型系统的概念。那么在Smalltalk里我们会怎么做呢?
对于playSoloOnInstrument:instrument,我们对于instrument的类型是有要求的,就是它必须能够接受playNote这个消息。当然这个类型需要是隐性,我也可以对等的写出静态面向对象的代码把这个隐性的类型显示的表示出来:
同样对于第二个方法我们也可以写出来:
如果我们需要多于一个的消息也是一样的,比如
同样这个时候隐性的类型需要就是
那么接口是什么呢?我给出一个不确切的说法,interface是一个消息的发送者(sender)和一个消息的接受者(reciver)间的一种类型的约定,也就是说在我看来interface的用处主要在细粒度的显式类型约定。我有一个同事,每次写代码都为一个Test Case所要测试的对象定义一个interface,每个interface都只有2-3个方法(lx同学夸你呢:D),这是很得interface之三味的用法。这种的做法对于在静态的面向对象系统的好处我们在继承里再论述。
至于老庄所说的接口是多继承的一种代替品,这只不过是世俗的看法,在静态类型的面向对象里,继承至少有2个语义:
1.实现继承,这个在Smalltalk,Ruby,C++里有,而在java里没有,C++里是通过private extends来实现的
这也是C++里为数不多的subclass不是subtype的例子。
2.类型继承,这个在C++和java里有,而在smalltalk,ruby有却不明显。
类型继承的极致就是C++里的纯虚父类
也就是java里的interface
因此,也就明了了,所谓“面向接口编程”是以类型作为约定的编程。我觉得这点大家一定不陌生,面向接口的编程里interface都很小而且约定明确。但是要说明一点的是,这个东西和"面向抽象而不要面向具体编程"其实还不一样,所以这个东西也就仅仅能算是静态类型面向对象的一个惯用法,还到不了原则这么高。