開篇之前先說明一下,我和老莊有著不錯的私交,他最初寫喪鐘系列的時候,我是忠實的擁躉之一,莊兄見我尚有寸尺所長,還在喪鐘系列里引用了我的幾個觀點。然而最近一段時間里,我作了這樣幾件事情:

1.重新學習了lambda演算。我的數學基礎不及taowen等,費了一些時日仍不完全了然,目前大抵能對著R5RS后面的語義定義對一些簡單的scheme程序進行lambda演算。

2.反復研究了幾遍SICP。同時看了錄像和書(感謝曹老師下的Video),自信對前二章半有些許認識書中未及,后二章半粗知大覽尚不能發前人所未發之言。

3.學習了Smalltalk和Ruby,Duck Typing以及其他一些有關類型系統的東西。

4.回顧了一下面向對象語言的一些發展史,以史為鑒粗知一些興替。

5.經日和taowen,老莊討論語言、設計等等之類,每為一辨必窮我所知爭發一言,所以知識可以迅速雜糅:D
經過種種之后,發現當初所謂鳴OO之喪鐘,實在是言過其實。
--------------------------------------------------------------------------------------------------------------------------------------------------------------
第0 關于"面向對象"
既然要為人家敲喪鐘,先要找對了苦主,不然豈不是白忙活了一場。什么是面向對象?這個問題太大,我們舉一個很常見的例子:
這個例子自然很不充分,不過繼承啊,接口啊,多態啊,略舉大概,尚可以作為一個討論的例子。從這個例子里,我們發現有這樣一個事實,就是Person類有兩個方法

void playSolo(Instrument instrument);

void playBackground(Instrument instrument);
分別表示,需要一個類型為Instrument的對象instrument,然后對他進行相應的操作,而且,從概念上來講,第一個方法,使演奏旋律的,也就是單音,而第二個方法是演奏和聲的。那么,如果我有一個類
我們知道,玻璃瓶裝了水也是可以發出聲音,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里,我們可以寫成:
然后就可以

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)。還是上次的例子:
1


interface Instrument

{
2

void playNote(Note note);
3

void playChord(Chord chord);
4

}
5

6

in Person Class
7

8


void playSolo(Instrument instrument)

{
9

10

}
在我接觸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這個消息。當然這個類型需要是隱性,我也可以對等的寫出靜態面向對象的代碼把這個隱性的類型顯示的表示出來:
1


interface Instrument

{
2

Instrument playNote(Note note);
3

}
同樣對于第二個方法我們也可以寫出來:
1


interface Instrument

{
2

Instrument playChord(Note note);
3

}
如果我們需要多于一個的消息也是一樣的,比如

Person>>playBWV996OnInstrument:instrument

instrument playNote: Note C;

playChord: Chord C;

playNote: Note D.
同樣這個時候隱性的類型需要就是
1


interface Instrument

{
2

Instrument playNote(Note note);
3

Instrument playChord(Note note);
4

}
那么接口是什么呢?我給出一個不確切的說法, interface是一個消息的發送者(sender)和一個消息的接受者(reciver)間的一種類型的約定 ,
也就是說在我看來interface的用處主要在細粒度的顯式類型約定。我有一個同事,每次寫代碼都為一個Test
Case所要測試的對象定義一個interface,每個interface都只有2-3個方法(lx同學夸你呢:D),這是很得interface之三
味的用法。這種的做法對于在靜態的面向對象系統的好處我們在繼承里再論述。
至于老莊所說的接口是多繼承的一種代替品,這只不過是世俗的看法,在靜態類型的面向對象里,繼承至少有2個語義:
1.實現繼承,這個在Smalltalk,Ruby,C++里有,而在java里沒有,C++里是通過private extends來實現的
1


class SubClassA: private ImplementationParent

{
2

}
這也是C++里為數不多的subclass不是subtype的例子。
2.類型繼承,這個在C++和java里有,而在smalltalk,ruby有卻不明顯。
類型繼承的極致就是C++里的純虛父類
1


abstract class Parent

{
2

3

public asbtract void method1() = 0;
4

public asbtract void method2() = 0;
5

}
也就是java里的interface
1


interface Parent

{
2

void method1();
3

void method2();
4

}
因此,也就明了了,所謂“面向接口編程”是以類型作為約定的編程。我覺得這點大家一定不陌生,面向接口的編程里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所寫一個魯特琴組曲,魯特琴是彈撥樂器,同時也是和聲樂器(所謂和聲樂器就是可以演奏和聲)。在很多樂器上都有改編的版本,比如魯特琴的近親吉他等等,這個時候,我們可以實現這樣幾個類
然后我們可以嘗試以此調用

vincent.playBWV996OnInstrument: Guitar new.

vincent.playBWV996OnInstrument: Lute new.

vincent.playBWV996OnInstrument: Bass new.
最后一個會得到一個messageNotUnderstand的錯誤。也就是說,對于Bass而言由于不能演奏和聲從而不能演奏BMV996(不過這個世界上能人太多了...哎),我們換到靜態類型面向對象系統來看。
對于第一個方法,playSolo的時候我們要求的類型是能夠演奏單音的。我們可以寫出來
1


interface SoloInstrument

{
2

SoloInstrument playNote(Note note);
3

}
對于第二個方法,playChord的時候我們要求的類型是能夠演奏和弦的,我們可以寫出來
1


interface ChordInstrument

{
2

ChordInstrument playChord(Chord note);
3

}
而對于第三個方法,playBWV996的時候我們要求既能演奏和弦也能演奏單音,這個時候出現一個問題,我們怎么處理Instrument的繼承關系?一個能演奏和弦的樂器是否可以演奏單音(答案是一般而言是的,但是也不排除有一些不是這樣的)?還是我們簡單的寫:
1


interface SoloAndChordInstrument extends SoloInstrument, ChordInstrument

{
2

}
或者
1


interface BWV996Playable

{
2

3

BWV996Playable playNote(Note note);
4

BWV996Playable playChord(Chord note);
5

}
對于動態類型簡單的隱性類型約定,顯示的類型系統帶來的一個副作用就是我們必須處理類型之間的關系。 注意這里是類型之間的關系,而不是對象之間的關系 。老莊同志批了很多篇的面向對象的抽象,面向對象的類型系統以及面向對象的本體論, 其實都在是在類型關系上折騰,而不是在對象關系上折騰 。而事實上面向對象的 類型系統并非必然就是靜態類型系統,而我們的類之間的關系不一定就和類型的關系相一致。 就像上例所示,在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 value4

valuen) (lambda (dispatch) (dispatch value1 value2 value3 value4

valuen)))

(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里都有不錯的體現。