开篇之前先说明一下,我和老庄有着不错的私交,他最初写丧钟系列的时候,我是忠实的拥趸之一,庄兄见我尚有寸尺所长,还在丧钟系列里引用了我的几个观点。然而最近一段时间里,我作了这样几件事情:
1.重新学习了lambda演算。我的数学基础不及taowen等,费了一些时日仍不完全了然,目前大抵能对着R5RS后面的语义定义对一些简单的scheme程序进行lambda演算。
2.反复研究了几遍SICP。同时看了录像和书(感谢曹老师下的Video),自信对前二章半有些许认识书中未及,后二章半粗知大览尚不能发前人所未发之言。
3.学习了Smalltalk和Ruby,Duck Typing以及其他一些有关类型系统的东西。
4.回顾了一下面向对象语言的一些发展史,以史为鉴粗知一些兴替。
5.经日和taowen,老庄讨论语言、设计等等之类,每为一辨必穷我所知争发一言,所以知识可以迅速杂糅:D
经过种种之后,发现当初所谓鸣OO之丧钟,实在是言过其实。
-------------------------------------------------------------------------------------------------------
第0 关于"面向对象"
既然要为人家敲丧钟,先要找对了苦主,不然岂不是白忙活了一场。什么是面向对象?这个问题太大,我们举一个很常见的例子:
interface Instrument {
void playNote(Note note);
void playChord(Chord chord);
}
abstrat class Keyboard implement Instrument {
..
}
class ClassicGuitar implements Instrument {
.
}
class Piano extends Keyboard {
..
}
in person class
void playSolo(Instrument instrument) {
instrument.playNote(AbsoluteNote.C1).playNote(AbsoluteNote.C2);
...
}
void playBackground(Instrument instrument) {
instrument.playChord(Chord.C5).playChord(Chord.C9);
...
}
in some case
ClassicGuitar perez711= ...
Piano pianoHere = ...
vincent.playSolo(stenzel);
may.playBackground(pianoHere);
etc.
这个例子自然很不充分,不过继承啊,接口啊,多态啊,略举大概,尚可以作为一个讨论的例子。从这个例子里,我们发现有这样一个事实,就是Person类有两个方法
void playSolo(Instrument instrument);
void playBackground(Instrument instrument);
分别表示,需要一个类型为Instrument的对象instrument,然后对他进行相应的操作,而且,从概念上来讲,第一个方法,使演奏旋律的,也就是单音,而第二个方法是演奏和声的。那么,如果我有一个类
class GlassBottle {
void playNote(Note note) {
..
}
}
我们知道,玻璃瓶装了水也是可以发出声音,Mozart可是为玻璃瓶写过曲子的,而这里我们却不能
GlassBottle beerBottle = ...
vincent.playSolo(beerBottle);
因为我们在构造Person类的时候,我们的play方法是依赖一个类型为Instrument的Object,换而言之,我们这里所谓的Object-Oriented其实称作Object-with-Type-Oriented的更合适,其实这类语言有一个专有名词,叫做static type object oriented。那Object-Oriented是不是就一定是Static Type Object Oriented的呢?不然,比如在Ruby里,我们可以写成:
class Person
def playSolo(thing)
thing.playNote(Note.C1).playNot(Note.c2)
end
def playBackground(thing)
thing.playChord(Chord.C5).playChord(Chord.C9)
end
end
class Guitar
def playNote(note)
...
end
def playChord(chord)
...
end
end
class GlassBottle
def playNote(note)
...
end
end
然后就可以
perez711 = Guitar.new
vincent = Person.new
vincent.playSolo(perez711)
同样也可以
vincent.playSolo(GlassBottle.new)
在这里,类型可以推演也可以留到runtime检查,如果是推演的话,在playNote里,由于thing调用了playNote方法,所以传进来的对象必须要刻意接受playNote这个消息,于是Guitar和GlassBottle都是可以通过,因此我即可以演奏我的Perez 711,也可以敲玻璃瓶。这种方式叫做dynamic type。
那么static type和dynamic type区别在那呢?static type认为,class就是type,type就是class; subclass就是subtyping,subtyping就是subclass(其实这句不严谨,在c++里可以使得subclass不是subtyping,但是java里就没办法了);而dynamic type则认为类型和class没有关系,类型取决于一个对象能够接受的消息。
那么哪个才是面向对象之真貌呢?遗憾的说,static type并不是最初的面向对象。以对象作为模块化的单位,始自Simula 67。但是Simula 67并不是一个面向对象系统,以目前的观点来看,充其量是Object-based,第一个面向对象语言当推Smalltalk,Smalltalk是dynamic type的。不过Smalltalk不太流行,第一个大面积流行的面向对象语言是C++,C++是static type的,正如Lisp是第一个函数式编程语言,很多Lisp的特性被当作函数式语言的共有特性(比如表是最重要的数据结构,轻语法)一样,所以很多人以为面向对象就必然是static type(比如老庄和我)。对于面向对象的误解,80%来自C++。也就是说,80%来自于static type的面向对象系统。将static type面向对象归咎于整个面向对象,我辈实在是大而无当言过其实。
后面我再一一将老庄所言之OO不当之处加以说明
第1 关于"接口"
关于接口的问题老庄说对了,这个东西并不属于面向对象的概念,而且在动态类型面向对象语言(比如Ruby, Smalltalk里),根本没有这个东西。这是一个纯粹的静态类型面向对象语言的特性,或者直接说,接口就是一个纯类型(Type)。还是上次的例子:
interface Instrument {
void playNote(Note note);
void playChord(Chord chord);
}
in Person Class
void playSolo(Instrument instrument) {
}
在我接触Smalltalk的最开始的一段时间里,这种地方是让我最难受,我已经习惯了用类型辅助我的思维,但是我发现我在Smalltalk里做不到,虽然我可以写出
Instrument>>playNote:note
^self subclassResponsibility.
Instrument>>playChord:chord
^self subclassResponsibility.
但是他却不是一个接口,我可以很用
instrument = Instrument new.
instrument playNote: Note C.
来构造一个Instrument之后在它之上调用playNote方法,然而我会得到一个messageNotUnderstand的错误,Smalltalk和Ruby里没有Abstract的概念。也就是说abstract method,abstract class以及interface,都不是面向对象的概念(或者严格一些说,都不是面向对象的必须的概念),而是面向对象类型系统的概念。那么在Smalltalk里我们会怎么做呢?
Person>>playSoloOnInstrument:instrument
instrument playNote: Note C;
playNote: Note D;
playNote: Note E.
Person>>playBackgroundOnInstrument:instrument
instrument playChord: Chord C1;
playChord: Chord C1;
playChord: Chord C1;
对于playSoloOnInstrument:instrument,我们对于instrument的类型是有要求的,就是它必须能够接受playNote这个消息。当然这个类型需要是隐性,我也可以对等的写出静态面向对象的代码把这个隐性的类型显示的表示出来:
interface Instrument {
Instrument playNote(Note note);
}
同样对于第二个方法我们也可以写出来:
interface Instrument {
Instrument playChord(Note note);
}
如果我们需要多于一个的消息也是一样的,比如
Person>>playBWV996OnInstrument:instrument
instrument playNote: Note C;
playChord: Chord C;
playNote: Note D.
同样这个时候隐性的类型需要就是
interface Instrument {
Instrument playNote(Note note);
Instrument playChord(Note note);
}
那么接口是什么呢?我给出一个不确切的说法,interface是一个消息的发送者(sender)和一个消息的接受者(reciver)间的一种类型的约定,也就是说在我看来interface的用处主要在细粒度的显式类型约定。我有一个同事,每次写代码都为一个Test Case所要测试的对象定义一个interface,每个interface都只有2-3个方法(lx同学夸你呢:D),这是很得interface之三味的用法。这种的做法对于在静态的面向对象系统的好处我们在继承里再论述。
至于老庄所说的接口是多继承的一种代替品,这只不过是世俗的看法,在静态类型的面向对象里,继承至少有2个语义:
1.实现继承,这个在Smalltalk,Ruby,C++里有,而在java里没有,C++里是通过private extends来实现的
class SubClassA: private ImplementationParent {
}
这也是C++里为数不多的subclass不是subtype的例子。
2.类型继承,这个在C++和java里有,而在smalltalk,ruby有却不明显。
类型继承的极致就是C++里的纯虚父类
abstract class Parent {
public asbtract void method1() = 0;
public asbtract void method2() = 0;
}
也就是java里的interface
interface Parent {
void method1();
void method2();
}
因此,也就明了了,所谓“面向接口编程”是以类型作为约定的编程。我觉得这点大家一定不陌生,面向接口的编程里interface都很小而且约定明确。但是要说明一点的是,这个东西和"面向抽象而不要面向具体编程"其实还不一样,所以这个东西也就仅仅能算是静态类型面向对象的一个惯用法,还到不了原则这么高。
第2 继承
前面已经说过了,继承至少有2个语义: 实现继承和类型继承,在说明这两个东西之前,我们继续来看上面的例子
Person>>playSoloOnInstrument:instrument
instrument playNote: Note C;
playNote: Note D;
playNote: Note E.
Person>>playBackgroundOnInstrument:instrument
instrument playChord: Chord C1;
playChord: Chord C1;
playChord: Chord C1;
Person>>playBWV996OnInstrument:instrument
instrument playNote: Note C;
playChord: Chord C;
playNote: Note D.
现在我们看playBWV996这个消息,BWV996是Bach所写一个鲁特琴组曲,鲁特琴是弹拨乐器,同时也是和声乐器(所谓和声乐器就是可以演奏和声)。在很多乐器上都有改编的版本,比如鲁特琴的近亲吉他等等,这个时候,我们可以实现这样几个类
Lute>>playNote:note
...
Lute>>playChord:note
...
Guitar>>playNote:note
...
Guitar>>playChord:note
...
Bass>>playNote:note
...
然后我们可以尝试以此调用
vincent.playBWV996OnInstrument: Guitar new.
vincent.playBWV996OnInstrument: Lute new.
vincent.playBWV996OnInstrument: Bass new.
最后一个会得到一个messageNotUnderstand的错误。也就是说,对于Bass而言由于不能演奏和声从而不能演奏BMV996(不过这个世界上能人太多了...哎),我们换到静态类型面向对象系统来看。
对于第一个方法,playSolo的时候我们要求的类型是能够演奏单音的。我们可以写出来
interface SoloInstrument {
SoloInstrument playNote(Note note);
}
对于第二个方法,playChord的时候我们要求的类型是能够演奏和弦的,我们可以写出来
interface ChordInstrument {
ChordInstrument playChord(Chord note);
}
而对于第三个方法,playBWV996的时候我们要求既能演奏和弦也能演奏单音,这个时候出现一个问题,我们怎么处理Instrument的继承关系?一个能演奏和弦的乐器是否可以演奏单音(答案是一般而言是的,但是也不排除有一些不是这样的)?还是我们简单的写:
interface SoloAndChordInstrument extends SoloInstrument, ChordInstrument{
}
或者
interface BWV996Playable {
BWV996Playable playNote(Note note);
BWV996Playable playChord(Chord note);
}
对于动态类型简单的隐性类型约定,显示的类型系统带来的一个副作用就是我们必须处理类型之间的关系。注意这里是类型之间的关系,而不是对象之间的关系。老庄同志批了很多篇的面向对象的抽象,面向对象的类型系统以及面向对象的本体论,其实都在是在类型关系上折腾,而不是在对象关系上折腾。而事实上面向对象的类型系统并非必然就是静态类型系统,而我们的类之间的关系不一定就和类型的关系相一致。就像上例所示,在Smalltalk里,Lute,Guitar和Bass之间没有任何的继承关系,但是对于person的3个消息而言,它们却是有类型的。
因此老庄所批的,是对象类型系统的抽象能力,而非面向对象的抽象能力。正如他在类型系统里所给的例子,那张他认为很失败的面向对象的图,其实可以完全不依赖继承来实现,而对这个类型系统的消费者而言,他们能够以一定的类型的观点,来处理这个系统的对象。
而老庄最后一个结论:
我的结论是:“一个类型,是由其本质决定了所能表现出的可操作性,而不是有其所能接受的操作决定了其本质。然而,OO正好把这个问题搞反了!”
我的看法是,这句话根本就是诡辩,前面半句的主语是“一个类型”,后面半句的主语是"OO"...
虽然前半句是对的,但是换一样说法可能更好:"所能接受的操作反映了其本质",面向对象本身就没有说我要做一个本质抽象,这一点在Smalltalk的类型判断操作上的可能是一个佐证,Smalltalk用isKindOf来判断继承关系,我们来玩一个文字游戏,改成俚语就是kinda,也就是"有一点,有几分"的意思,而不是说,“就是”,或者“从分类学上可证明之类的含义”。我再举一个龌龊的例子。
vincent ballon: AirBallon new.
vincent ballon: Condom new.
气球和保险套,对于ballon这个方法而言是一个类型,都是"有几分"可以吹起来。但是我怎么定义一个精确的本质?Ballonable?还是MakeFromLatexAndVeryThin?或者简单说FlexableAndThin?
在继承这一点上,我想老庄引文中:Elminster的话是从事物的特征与属性归纳出它的“类型”。恰恰对于静态类型面向对象系统是可行的。如我前文所述,我把一个object和所有sender的约定(也就是interface),继承在一起,恰恰就是一个颇为恰当的类型定义。
而对于动态类型系统里的面向对象语言,继承的也有类型继承的含义,但是并不是唯一的途径。用一句我们常说的话,在静态类型系统里,类型和类是紧耦合的,动态类型系统中他们的耦合比较松。
从此而观,所有对于面向对象的哲学考虑以及本体的思考,对于动态面向对象系统已经不是那么迫切了。而把对象类型系统的不足归咎于面向对象的不足,也似乎论据不足。
第3. 一切皆对象和面向对象的理论基础
老庄是反对一切皆对象的,而TrustNo1在javaeye的一篇帖子上说:
第一,我可以很负责的说,OO的,70年代成型,80年代在理论基础上就给人毙掉。从这种意义上说不是OO死不死的问题,而是OO还活着么?当然理论基础给人毙掉,不是说没有用。
我先说面向对象的理论基础的问题,至于一切皆对象稍后再表。
所谓面向对象的理论基础其实是没有的,原因很简单,面向对象根本就不是一种计算模型。在第一次软件危机的那个时代,对与计算机的非数值计算应用的讨论以及对于可计算性问题的研究和发展,大抵确立了几种主流的计算模型:递归函数类,图灵机,Lambda演算,Horn子句,Post系统等等。
其中递归函数类是可计算性问题的数学解释;图灵机是图灵解决可计算问题的时候所设计的装置,其后成为计算机的装置模型,与图灵机相关的形式语言和自动机成为了命令式语言的理论基础;lambda演算成为了函数式语言的理论基础;Horn子句是prolog这类逻辑语言的理论基础。但是我们惊讶的发现,面向对象没有计算模型的理论基础,换而言之,面向对象根本就不是从可计算性的研究上发展过来的,那么面向对象的理论基础的价值本身就不大。
所以我很奇怪的一个问题就是TrustNo1所谓的面向对象在80年代理论基础上给人毙掉的说法是从何而来的?既然面向对象本质上不是一种计算模型,那么它大抵上只能归结为一种应用技术,应用技术自然可以从各种不同的领域里得到相似的应用,那么毙掉的理论基础所指的又是什么呢?甚怪之。
既然面向对象不是一个计算模型,那么我们可以从不同的角度推断出OO的各种形态,老庄已经出给了从ADT引出OO的问题以及例子,我就不罗嗦了,我给一个从Functional Programming出来的例子,其实就是SICP里的Data as Procedure。
(define (make-user name age sex)
(define (dispatch message)
(cond ((eq? message 'getName) name)
((eq? message 'getAge) age)
((eq? message 'getSex) sex))
(else (error 'messageNotUnderstand))))
dispatch)
然后我们就可以
(define vincent (make-user 'Vincent 24 'Male))
(vincent 'getName)
自然的,如果我调用
(vincent 'sayHi)
会得到一个messageNotUnderstand的runtime错误,这就是一个很自然dyanmic type的对象封装,最早的面向对象系统Smalltalk和CLOS基本上都是这个路子,于是有一个问题,为什么最早的面向对象系统都是dyanmic type?这里就跟lambda演算有关了。
lambda演算这个计算模型根本的一个假设就是,对于任何一个定义良好的数学函数,我都可以使用lambda抽象来表述他的求值,因此无论是什么东西你能够构造lambda抽象的话,我就能计算。这个地方东西很多,大家可以找找lambda演算相关的资料,这里我说三件事(由于lambda太难输入,我用scheme程序代替,然后由于alpha变化,beta规约和eta规约我也用scheme伪码来模拟。)
第一个是数值的表述,其实这个很简单,不考虑丘奇代数的系统的话,我们可以把数值表示成单值函数:
(define one (lambda (x) 1))
这个东西无论给什么x都返回1,然后根据lambda演算里的alpha变换,这个lambda抽象等价于数值1。因此,对于所有的数值,我们可以按lambda演算处理。
第二个是bool的表达,也就是如何逻辑进行lambda抽象,下面我直接给出了,缺省认为大家都看了SICP,对Scheme也颇有心得。
(define true-new (lambda (x y) x)) ;;;这个函数也叫select-first
(define false-new (lambda (x y) x));;;这个函数也叫select-second
(define if-new (lambda (conditon if-true if-false) (condition if-true if-false)))
然后我就可以做一个测试
(if-new true-new 3 4)
3
(if-new false-new 3 4)
4
因此,对于所有bool我们可以按lambda演算来处理
第三个是自定义类型,这里我们还是先看一个Lisp里的例子,序对。
(define (cons a b) (lambda (dispath) (dispatch a b)))
(define (car list) (list select-first))
(define (cdr list) (list select-second))
这里依旧是high-order,我们来测试
(define list1 (cons 1 2))
(car list1)
1
(cdr list1)
2
这里大家自己用beta规约算一下,就发现的确是这样的。这里我们又可以看到,在lambda演算里,根本没有数据或者类型。有的永远各种各样的lambda抽象而已(目前已经有了带类型的lambda演算)。这里说一句题外话,SICP里的Data as Procedure其实就是在说这个问题,不过他没明说,而是用了一种特殊的用法而引出了消息传递风格,我觉得这里SICP有些顾此失彼,对于data as procedure我总结的一般形式是
(define (construct-function value1 value2 value3 value4valuen) (lambda (dispatch) (dispatch value1 value2 value3 value4valuen)))
(define (select-function1 data) (data select-first))
(define (select-function2 data) (data select-second))
...
(define (select-functionn data) (data select-last))
综上所述,我们看到在lambda演算里,一切都是lambda抽象,然后对于不同的lambda抽象使用alpha变换,beta规约和eta规约,表述各种不同计算。看,在面向对象之前就已经有了一切皆某某的完美的计算理论存在了。而且回顾一下:
(define (make-user name age sex)
(define (dispatch message)
(cond ((eq? message 'getName) name)
((eq? message 'getAge) age)
((eq? message 'getSex) sex))
(else (error 'messageNotUnderstand))))
dispatch)
(define vincent (make-user 'Vincent 24 'Male))
(vincent 'getName)
我们有理由说,对象其实就是一个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里都有不错的体现。