OO三步曲

注:文章所有權歸作者 倪 碩 所有,歡迎任何形式的轉載,不過請註明原始文章作者以及出處地點:http://nishuo.35123.net.並且歡迎訪問MatrixCpp的專欄
------------------------------------------

 OO三步曲

 

前言:面向對象程序設計(Object-Oriented Programming,以下簡稱OOP)是一種起源於六十年代的Simula語言,發展已經將近三十年的程序設計思想。其自身理論已經十分完善,並被多種面向對象程序設計語言(Object-Oriented Programming Language,以下簡稱OOPL)實現。如果把Unix系統看成是國外在系統軟件方面的文化根基,那麼Smalltalk語言無疑在OOPL領域和Unix持有相同地位。由於很多原因,國內大部分程序設計人員並沒有很深的OOP以及OOPL理論,很多人從一開始學習到工作很多年都只是接觸到c/c++,java,vb,delphi等靜態類型語言,而對純粹的OOP思想以及作爲OOPL根基的Smalltalk以及動態類型語言知之甚少,不知道其實世界上還有一些可以針對變量不綁定類型的編程語言。而這些對比卻是深刻理解OO理論的重要部分,而國內這方面的資料也爲數不多。故把自己的一些OO學習心得寫下來做爲一個系列文章(一共三篇,第一篇描敘OOP的一些基本但容易被誤解的理論,第二篇主要說明各種OOPL演化和發展以及對於OOP理論的支持,第三篇主要是說模式和組件在OOP中的地位以及展望OOP的未來),由於文章描敘的只是自己對於OOP/OOPL的理解,錯誤以及淺薄之處再所難免,只是希望對大家能起到拋磚引玉的作用。

 

 

                              淺析OO的基石

 

從抽象說起

 

       Booch曾經在他自己的OO領域內的名著[Booch 94]中開篇就論敘到了複雜性是軟件開發過程中所故有的特質。而人們處理複雜性的最根本武器就是抽象。廣義的抽象代表的是對複雜系統的簡化描敘或規格說明,爲了突出系統的本質屬性而故意忽略其中的非實質性細節。“一個概念只有當能被最終用來實現的機制獨立的描敘,理解,分析時,纔將這個概念限定爲抽象的概念”。而Booch也給出了他心目中關於OO領域內的狹義抽象定義:“抽象表示一個對象與其他所有對象相區別的基本特徵,因此提供了同觀察者角度有關的清晰定義的概念界限。”因此,根據不同觀察角度,我們可以針對OOP給出不同級別的抽象層次。通常,面對一個典型的面向對象程序,[Budd 2002]將其分成五個抽象層,分別覆蓋了OOP中的分析,設計與編程的各個階段:

1,  最高級別的抽象層上,程序被看成是由很多相互作用並且遵守契約的對象所組成的對象集合。對象之間相互合作完成程序的計算任務。這個抽象級別上的典型代表就是設計模式思想(Design Pattern)。

2,  第二個抽象層就是一個對象集單元,也就是一羣定義之間有相互聯繫的對象,在程序設計語言級別來看Java中是packages,C++中是name space。這個抽象級別上的典型代表就是模塊化思想(Modularity)。

3,  第三個抽象層所代表的是典型的OOP模式:客戶/服務器模型,這主要是用來抽象兩個對象之間的互交過程。在這個抽象級別上的典型代表就是對象之間的消息機制(Message Passing)。

4,  第四個抽象層就是針對一組相似對象定義一個類作爲生成對象的模板,類定義了對象的對外使用接口以及繼承對象所需的內部繼承接口,而這個抽象層次的典型代表就是接口編程(Interface Programming)。

5,  第五個抽象層就是實現一個類所需要的方法和成員變量的實現(Implementation)。在這裏OOP最終和POP(Procedure-Oriented Programming)相融合。

當然,我們可以根據各自的觀察角度劃分成更細的抽象層次比如說針對第五層抽象用到的POP理論,我們還可以進一步的劃分出控制抽象(三種完全描敘圖靈機計算模型所需要的控制結構)以及數據抽象(ADTs)等等,並由此繼續下去(如果你的想象力足夠豐富的話:)。

 

 

什麼是OOP?

 

       OOP的許多原始思想都來之於Simula語言,並在Smalltalk語言的完善和標準化過程中得到更多的擴展和對以前的思想的重新註解。可以說OO思想和OOPL幾乎是同步發展相互促進的。與函數式程序設計(functional-programming)和邏輯式程序設計(logic-programming)所代表的接近於機器的實際計算模型所不同的是,OOP幾乎沒有引入精確的數學描敘,而是傾向於建立一個對象模型,它能夠近似的反映應用領域內的實體之間的關係,其本質是更接近於一種人類認知事物所採用的哲學觀的計算模型。由此,導致了一個自然的話題,那就是OOP到底是什麼?[D&T 1988][B.S 1991] .。在OOP中,對象作爲計算主體,擁有自己的名稱,狀態以及接受外界消息的接口。在對象模型中,產生新對象,舊對象銷燬,發送消息,響應消息就構成OOP計算模型的根本。

 

       對象的產生有兩種基本方式。一種是以原型(prototype)對象爲基礎產生新的對象。一種是以類(class)爲基礎產生新對象。原型的概念已經在認知心理學中被用來解釋概念學習的遞增特性,原型模型本身就是企圖通過提供一個有代表性的對象爲基礎來產生各種新的對象,並由此繼續產生更符合實際應用的對象。而原型-委託也是OOP中的對象抽象,代碼共享機制中的一種。一個類提供了一個或者多個對象的通用性描敘。從形式化的觀點看,類與類型有關,因此一個類相當於是從該類中產生的實例的集合。而這樣的觀點也會帶來一些矛盾,比較典型的就是在繼承體系下,子集(子類)對象和父集(父類)對象之間的行爲相融性可能很難達到,這也就是OOP中常被引用的---子類型(subtype)不等於子類(subclass)[Budd 2002]。而在一種所有皆對象的世界觀背景下,在類模型基礎上還誕生出了一種擁有元類(metaclass)的新對象模型。即類本身也是一種其他類的對象。以上三種根本不同的觀點各自定義了三種基於類(class-based),基於原型(prototype-based)和基於元類(metaclass-based)的對象模型。而這三種對象模型也就導致了許多不同的程序設計語言(如果我們暫時把靜態與動態的差別放在一邊)。是的,我們經常接觸的C++,Java都是使用基於類的對象模型,但除此之外還有很多我們所沒有接觸的OOPL採用了完全不一樣的對象模型,他們是在用另外一種觀點詮釋OOP的內涵。

 

 

什麼是類型(type)?

 

       類型以及類型系統的起源以及研究與發展是獨立於OOP的。早在五十年代的FORTRAN語言編譯器實現中,就已經採用類型系統作爲類型檢查的一種手段。廣義的類型一般被定義爲一種約束,也就是一種邏輯公式。而在對類型的研究過程中產生多種方法,比如[C&W 1985]等。而代數方法(algebraic approach)是一種非常好的建立類型的形式化規範的方法。代數中的一個類型對應於一系列元素,在它們之上定義代數操作。同時在此基礎上二階λ演算已經被用於繼承和模板所支持的模型。在上面兩種方法中,類型被認爲是一系列滿足確定約束條件的元素,更抽象的方式可以把一個類型當作規定一個約束條件,如果我們規定的約束條件越好,相對應的被定義元素的集合就越精密,所以邏輯公式(logical formulas)就成爲描敘類型特徵的最合適工具。在這裏,我們不想深入的探究對於類型理論的各種不同的數學模型,我們需要明白的是類型(type)以及類型理論這個在編程語言中經常應用到的概念的內涵是極其豐富的,而其自身理論的發展並非侷限於OOP之中,但當兩者相結合的時候就對我們的程序觀產生了巨大的影響。

 

類(class),類型(type),接口(interface)

 

       這三個概念是在OOP中出現頻率最多,也最容易混淆的。而對於這三個概念的澄清也是文章寫作的初衷。讓我們先看看大師們對於這三個概念的描敘----

“The fundamental unit of programming in Java programming language is the class, but the fundamental unit of the object-oriented design is the type.while classes define types,it is very useful and powerful to be able to define a type without defining a class.Interface define types in an abstract form as a collection of methods or other types that form the contract for the type.” [Jams 2000]。

“In C++,A class is a user definite type”[B.S 1998]。

“A type is a name used to denote a particular interface……An object may have many types,and widely different objects can share a type.Part of an object’s interface may be characterized by one type ,and other parts by other types.Two objects of the same type need only share parts of their interface.Interface can contain other interface as subset.We say that a type is a subtype of another if its interface contain the interface of its supertype.Often we speak of a subtype inheriting the interface of its supertype”[Gamma 1995]

 

在其中,一共出現了四個概念:類(class),類型(type),接口(interface)以及契約(contract)。這裏我們說到的類型和上面提到的類型有所不同,是狹義的OOP中的類型。爲了理解這幾個概念,我先劃分出三個概念域:一個是針對現實世界的,一個是針對特定程序設計範型的(在這裏就是OO設計範型),最後一個是針對編譯器實現的。也就是說,在現實世界中的概念必須有一種手段映射到OO範型中去,而OO範型中的概念也應該在編譯器實現中有相同的概念對應。由此,我們可以這樣說,類是做爲現實世界中的概念,而傳統的OOPL都會提供class關鍵字來表示對現實世界模擬的支持。而接口,是作爲OO程序設計範型中與類對應的一個概念。在OO設計中,我們所要做的就是針對接口進行設計和編程,而接口的實質含義就是對象之間的一種契約。而類型就是編譯器實現中針對類和接口所定義的對應概念。可以這樣說,類是現實世界中存在的客觀概念,是唯物的。接口是設計人員定義出來的,存在於設計人員心中的概念,是唯心的。而類型是類和接口這兩種概念的編譯器實現的映射概念,也是唯物的。類型主要是用來指導編譯器的類型檢查的謂詞,類是創建現實對象的模板,接口是OO設計中的關鍵概念。這三個概念相互區別(分別位於不同的概念域),又相互聯繫(都是代表相同的概念的不同概念域的映射)。有了上面的理解,我們看看下面最常見的Java語句:

       people a=new man();

這代表了什麼?程序員向編譯器聲明瞭一個people類型(type)的對象變量a,而對象變量a本身卻指向了一個man類(class)的實體(而在編譯器中理解是對象變量a指向了一個類型爲man的實體)。再讓我們回到[Jams 2000],其中句子的根本含義我們可以概括如下:聲明一個類或者一個接口都同時向編譯器註冊了一個新的類型,而此類或者接口以及類型都是共享同樣的一個名字。也就是說。編譯器所能理解的全部都是類型,而程序員的工作是把現實中的類概念轉化爲設計中的接口概念,而編譯器對應於上兩種概念都有直接的支持,那就是一個類聲明或者接口聲明在編譯器的理解來看就是一個類型聲明。但是反過來卻不一定成立。一個類可以有多個接口(一個類完全有可能實現了設計人員的多個契約條件),同時也就可能有多個類型(因爲類型不過是接口這個設計域內的概念在編譯器中的實現)。

 

 

 

多態,替換原則,對象切割

 

       多態作爲OO中的核心機制之一擁有着豐富的內涵。顧名思義,多態就是一種名稱多種形態的意思。其主要有三種形式:函數多態,對象變量多態,泛型多態。函數多態主要包括函數重載(overload)和改寫(overriding)。泛型多態(genericity)主要是提供了一種創建通用工具的方法,可以在特定的場合將其特化。在這裏,我們重點要考量的是對象變量多態。在理解對象變量多態之前,我們首先了解一下OO核心機制之一的替換原則。靜態類型的OOPL的一個特徵就是一個變量所包含的值(value)的類型可能並不等於這個變量所聲明的類型,在傳統的編程語言中並不具備這樣的特徵,因爲我們不可能把聲明爲整型的變量賦上字符串的變量值。而替換原則發生作用的情況就隱含的描敘了兩種不同類型所具有的關聯----類型繼承。Barbara Liskov曾經這樣描敘替換原則以及起作用的類型之間的關聯:對於類型爲S的每個對象s,存在一個類型爲T的對象t,對於根據類型T所定義的所有程序P,如果用對象s替換對象t,程序P的行爲保持不變,那麼類型S就是類型T的子類型[Liskov 1988]

 

       在理解了多態以及替換原則後,我們可以繼續深入理解繼承與替換原則相結合所帶來的新的觀點。可以說繼承與替換原則的引入影響了幾乎所有的OOPL,包括類型系統,值語義/引用語義,對象內存空間分配等等。下面,我將試圖逐步的撥開其中的各種因果。

 

       首先考慮,people a; 這樣的代碼在編譯器中將如何實現?可以肯定是首先將把類型people綁定到對象a上,然後必須爲對象a分配空間。同時,我們創建people的子類man,由於man IS A people。根據多態以及替換原則,我們當然可以讓對象a保存一個man類型的值(這就是替換原則的表現)。這是一種直觀的描敘,但在編程語言的實現過程中就出現一些困難。我們知道繼承是一種擴展接口與實現的方式,那麼我們就很難保證man類型不對people類型做擴展,而一旦做出擴展,我們如何能用存儲people對象的空間去存儲man類型的對象值呢?

       people a;

man b=new man();

a=b;

這樣的代碼將首先把b對象進行切割,然後再存儲到a對象空間去。然而這並不是我們所期望的。那麼,爲了支持OOP的繼承,多態,替換原則,但卻需要避免對象切割的發生,面對對象a我們將採用何種分配空間模型呢?常用的有下面三種方式:

1,  只爲a分配基類people的存儲空間,不支持對象多態以及替換原則。這樣的模型內存分配緊湊,存儲效率很高。

2, 分配繼承樹中的最大類對象所需要空間(在這裏是man類的對象值空間),這樣的模型簡單,同時可以實現多態和替換原則而避免對象切割問題,但是十分浪費內存空間十分明顯。

3,  只分配用於保存一個指針所需要的存儲空間,在運行時通過堆來分配對象實際類型所需要的空間大小,這樣也可以實現多態和替換原則而避免對象切割問題。(也就是說a只是一個對象的引用,而不是真實的對象,真實對象的生成必須靠程序員顯式的聲明)。

對於上面提到的三種內存模型,1和3都被一些程序設計語言所採用。相信說到這裏,大家應該開始慢慢明白了。是的,C++作爲C語言的繼承者,對於效率的追求迫使它必須採用第一種最小靜態空間分配的方式,由於基於棧空間的程序運行效率要比基於堆空間的程序運行效率高出許多,所以C++允許用棧空間保存對象,但同時也允許堆空間保存對象,可以說C++是採用了1和3兩種相混合的內存模型,而C++中基於1內存模型的對象,也就是說基於棧內存空間的對象是沒有辦法體現多態和替換原則的(請思考一下在C++中什麼對象是基於棧的),而基於3內存模型的對象將支持多態和替換原則(又想一想在C++中什麼對象是基於堆的)。這裏,我們終於可以揭開第一層迷霧了,很多人都知道在C++中只有指針和引用才能支持對象的多態行爲,但是爲什麼會如此?上面做出了最好的解釋。

 

Java語言由於設計理念和C++有着很大的區別,它採用的是第3種對象模型,一切對象(除了基本類型對象)都是基於堆分配的。這也是Java語言必須採用GC的原因所在。在C++中很大一部分對象是不需要程序員進行管理的(靜態空間對象),而在Java中,如果不採用GC機制,所有的對象都需要程序員管理,而這樣的開發代價將是巨大而不現實的。這也就揭開了第二層迷霧,當我們在對比C++和Java語言的時候總是爲GC是否有其價值而爭論不休,但當你拋開所謂的好與不好的簡單討論,進入到其語言本身的內在對象存儲本質的時候,也許對於各種聲音纔會有一個屬於自己的清醒認識。

 

讓我們繼續望下走,不同的對象內存分配模型直接影響到其程序設計語言的賦值的含義。在各種編程語言中,賦值可以給出兩種不同的語義解釋:複製語義和指針語義。很明顯,由於C++支持兩種相混合的對象存儲模型(但是默認的存儲方式是棧存儲),所以在C++中默認賦值語義採用的是前者,但C++同時提供了指針語義的功能支持(在拷貝構造函數和=運算符重載中用戶進行自定義)。而在Java中採用的是後者。這也就是我們揭開的最後一道迷霧,不同的對象存儲模型直接導致了不同的賦值語義。

 

 

面向對象的計算模型和可計算性

 

       編程就是用計算所需要的指令構成一種運算裝置,無論我們的程序設計思想以及程序設計語言如何發展和提高,最終我們所使用的底層計算數學模型並沒有改變。但高級程序設計語言給我們帶來的變革是在其語言環境中構建起了一個全新的,更抽象的虛擬計算模型。Smalltalk語言引入的對象計算模型從根本上改變了以前的傳統計算模型,以前的計算模型突出的是順序計算過程中的機器狀態,而現在的對象計算模型突出的對象之間的協作其計算結果由參加計算的所有的對象的狀態總體構成。而由於對象本身具有自身狀態,我們也可以把一個對象看成是一個小的計算機器。這樣,面向對象的計算模型就演變成了許多小的計算機器的合作計算模型。圖靈機作爲計算領域內的根本計算模型,精確的抓住了計算的要點:什麼是可計算的,計算時間和空間存儲大小開銷有多大。計算模型清楚的界定了可計算性的範圍,也就界定了哪些問題是可求解,哪些問題是不可求解的。OOP爲程序員提供了一種更加抽象和易於理解的新的計算模型,但其本身並沒有超越馮.諾依曼體系所代表的圖靈機數學計算模型。所以我們不能期望OOP能幫助我們解決更多的問題,或者減少運算的複雜度。但OOP卻能幫助我們用一種更容易被我們所理解和接受的方式去描敘和解決現實問題。

 

 

結 束

 

       這篇文章做爲這個系列的第一篇,對於OOP中的許多核心概念和機制進行了有益的討論,作者衷心的希望通過這篇文章能夠讓大家對於OOP有更深入的理解,同時明白OOP作爲已經發展將近三十年的程序設計思想,其自身豐富的理論內涵不是單單學習幾門OOPL就可以領悟。最後期望本文能實現了它的初衷---拋磚引玉。

 

 

Reference:

 

[D&T 1988] : Type Theories and Object-Oriented programming by Scott Danforth and Chris Tomlinson on ACM Computing Surveys Vol.20  No.1  March 1988

 

[Liskov 1988] :Data Abstraction and Hierarchy by Barbara Liskov on Sigplan Notices,23(5),1988

 

[C&W 1985] On understanding types,data abstraction,and polymorphism by Cardelli . L and Wegner . P on  ACM Computing Surveys Vol.17 No.4 Dec 1985

 

[B.S 1991] What is “Object-Oriented programming”?(1991 revised version) by Bjarne Stroustrup AT&T Bell Lab Murray Hill ,New Jersey 07974

 

[B.S 1998] 《The C++ programming language (Special Editon)》 by Bjarne Stroustrup Addison Wesley 1998

 

[Gamma 1995] 《Design Pattern》by Eric Gamma etc Addison Wesley 1995

 

[Booch 94] : 《Object-Oriented Analysis and Design with Application (Sec Editon)》 by Grady Booch Addison Wesley 1994

 

[Jams 2000] : 《The Java programming language (Third Editon)》by Ken Arnold, Jams Gosling, David Holmes Addison Wesley 2000

 

[Budd 2002]: 《Introduction to Object-Oriented programming (Third Editon)》 by Timothy A.Budd  Addison Wesley 2002

 

參考書籍:

 

《C++程序設計語言》Bjarne Stroustrup著 裘宗燕譯 機械工業出版社 2002年

 

《設計模式》Erich Gamma等著 李英軍等譯 機械工業出版社  2000年

 

《面向對象軟件開發原理》Anton著 袁兆山等譯 機械工業出版社 2003年

 

《面向對象編程導論》Timothy A.Budd著 黃明軍等譯 機械工業出版社 2003年

 

《面向對象分析與設計》 Grady Booch著 馮博琴等譯 機械工業出版社 2003年

 

《面向對象軟件構造》(英文版.第二版) Bertrand Meyer著 機械工業出版社 2003年

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章