天天看點

軟體設計本質論——白話面向對象

   不同的人在談面向對象程式設計(OOP)時所指的含義并不相同。有人認為任何采用圖形界面的應用程式都是面向對象的。有人把它作為術語來描述一種特别的程序間通信機制。還有人使用這個詞彙是另有深義的,他們其實是想說:“來啊,買我的産品吧!”我一般不提OOP,但隻要提到,我的意思是指使用繼承和動态綁定的程式設計方式。 --《C++沉思錄》

  《C++沉思錄》說的是十幾年前的事了,現在大家對面向對象的回答已經是衆口一詞:封裝、繼承和多态。大家都知道,在面向對象中,一輛汽車是一個對象,汽車這個概念是一個類。汽車有漂亮的外觀,把各種内部原理都隐藏起來了,司機不必知道它的内部工作原理仍然能開車,即使汽車随技術的進步不斷更新,對司機也沒有什麼影響,這就是封裝的好處。

  汽車是交通工具的一種,汽車是一個類,交通工具也是一個類,而交通工具類包括了汽車類,進而具有更廣泛的意義。這種從抽象到具體的關系就是繼承關系,我們可以說汽車類繼承了交通工具類,汽車類是交通工具類的子類,交通工具類是汽車類的父類。

  作為交通工具,它肯定可以運動(move),從甲地運動到乙地,就起到了交通的作用。輪船是一種交通工具,是以輪船類也是交通工具類的子類。同樣是運動,輪船的運動和汽車的運動方式肯定有所不同,這樣以不同的方式完成同樣的功能就叫多态。

  關于對象:對象就是某一具體的事物,比如一個蘋果, 一台電腦都是一個對象。每個對象都是唯一的,兩個蘋果,無論它們的外觀有多麼相像,内部成分有多麼相似,兩個蘋果畢竟是兩個蘋果,它們是兩個不同的對象。對象可以是一個實物,也可能是一個概念,比如某個蘋果對象是實物,而一項政策可能就是一個概念性的對象了。

  關于類:對象可能是一個無窮的集合,用枚舉的方式來表示對象集合不太現實。抽象出對象的特征和功能,按此标準将對象分類,這就引入類的概念。類就是一類事物的統稱,類實際上就是一個分類的标準,符合這個分類标準的對象都屬于這個類。當然,為了友善起見,通常隻需要抽取那些,對目前應用來說是有用的特征和功能。

  關于抽象類:類是對對象的抽象,比如,蘋果是對所有具體的蘋果的抽象。如果我們對蘋果這個類進行一步抽象,可以得到一個水果類。這種對類本身進行抽象而得到的類,就是抽象類。抽象類不像普通類,它是沒有對象與之對應的。像蘋果類,你總是可以拿到一個叫蘋果的東西,而對于水果類,根本沒一個真正叫水果的東西。你可以說一個蘋果是一個水果,從邏輯上講沒有錯,但沒有什麼意義。一般在程式中,抽象類是不能執行個體化的。

  關于面向對象:面向對象就是以對象為中心。為什麼不說是面對類,而說是面向對象呢?類是對象的集合,考慮類實際上也是在考慮對象,有時甚至并不嚴格的區分它們。是以說面向對象一詞比面向類更确切。

  既然以對象為中心,面向對象所考慮的内容自然是對象、對象間的協作、對象的分類、類之間的關系等等,由此引申了出幾個重要的概念。

  1. 封裝

  what:對象也有隐私,對象的隐私就是對象内部的實作細節。要想對象保持良好的形象就要保護好對象隐私,所謂的封裝其實就是保護對象隐私。當然,沒有人能完全隐藏自己的隐私,比如你去轉戶口時,你不得不透露自己的家庭資訊和健康狀況。另外,在不同的場合所透露隐私的數量也不一樣,朋友和家人可能會知道你更多隐私,同僚次之,其他人則知道得更少。面向對象也考慮了這些實際的情況,是以像C++之類的語言有public/private/protected/friend等關鍵字,以适應于不同的情況。

  why:封裝可以隔離變化。據以往的經驗,我們知道内部實作是容易變化的,比如電腦在不斷的更新,機箱還是方的,但裡面裝的CPU和記憶體已是今非昔比了。變化是不可避免的,但變化所影響的範圍是可以控制的,不管CPU怎麼變,它不應該影響使用者使用的方式。封裝是隔離變化的好辦法,用機箱把CPU和記憶體等等封裝起來,對外隻提供一些标準的接口,如USB插口、網線插口和顯示器插口等等,隻要這些接口不變,内部怎麼變,也不會影響使用者的使用方式。

  封裝可以提高易用性。封裝後隻暴露最少的資訊給使用者,對外接口清晰,使用更友善,更具使用者友好性。試想,如果普通使用者都要知道機箱内部各種晶片和跳線,那是多麼恐怖的事情,到現在為止我甚至還搞不清楚硬碟的跳線設定,幸好我沒有必要知道。

  how:在C語言中,可以用結構+函數來模拟類的實作,而用這種結構定義的變量就是對象。封裝有兩層含義,其一是隐藏内部行為,即隐藏内部函數,調用者隻能看到對外提供的公共函數。其二是隐藏内部資訊,即隐藏内部資料成員。現在都建議不要對外公開任何資料成員,即使外部需要知道的資料成員,也隻能通過函數擷取。

  在C語言中要隐藏内部函數很簡單:不要它把放在頭檔案中,在C檔案中定義時,前面加static關鍵字,每個類放在獨立的檔案中。這樣可以把函數的作用範圍限于目前檔案内,目前檔案隻有類本身的實作,即隻有目前的類自己才能看到這些函數,這就達到了隐藏的目的。

  在C語言中要隐藏資料成員較為麻煩,它沒有提供像C++中所擁有的public/protected/friend/private類似的關鍵字。隻能通過一些特殊方法模拟部分效果,我常用的方法有兩種。

  其一是利用C的特殊文法,在頭檔案中提前聲明結構,在C檔案才真正定義它。這樣可以把結構的全部資料資訊都隐藏起來。因為外部不知道對象所占記憶體的大小,是以不能靜态的建立該類的對象,隻能調用類提供的建立函數才能建立。這種方法的缺陷是不支援繼承,因為子類中得不到任何關于父類的資訊。

  

  其二是把私有資料資訊放在一個不透明的priv變量中。隻有類的實作代碼才知道priv的真正定義。

  2. 繼承

  what: 繼承描述的是一種抽象到具體的關系。具體的東西繼承了抽象的東西的特性,比如說,水果這個概念比蘋果這個概念更抽象,其意義更具有一般性,而蘋果這個概念則更具體,其意義更狹窄一些,在面向對象裡,我們可以說蘋果類繼承了水果類。繼承是指繼承了父類的特性,繼承本質是源于分類學,細的分類繼承大分類的特性。

  why: 繼承描述了抽象到具體的關系,是以能夠有效利用抽象這件武器來戰勝軟體的複雜性。抽象在實作中無處不在,類就是對事物的抽象,提到蘋果你就想到蘋果這一類事物,無需要關心其大小、顔色和成分,蘋果這兩個字就足夠了。名不正則言不順,言不順則事不成,看來老夫子已經領悟到了抽象的威力。

  繼承不但利用了抽象的力量來降低系統的複雜性,它還提供了一種重用的方式。假設我們承認下列面這個繼承關系,蘋果繼承了水果,水果繼承了食物,如果我們已經知道什麼是食物,什麼是水果,在描述蘋果時,沒有必要去重複講解食物和水果的概念了,這就是重用,重用了對水果和食物兩個概念的了解。

  how: 在C語言中實作繼承很簡單,可以用結構來模拟。這種實作基于一個明顯的事實,結構在記憶體中的布局與結構的聲明具有一緻的順序。我們知道在程式描述事物的特征時,主要通過資料變量描述事物的屬性特征,如顔色、重量和體積等,用函數來描述事物的行為特征,和運動、成長和搏鬥等。

  繼承

  繼承在現實世界中應用很廣,在程式裡也是一樣,甚至可以說是過度使用了。多年以前一些大師已經提出,優先使用組合而不是繼承。主要原因有三點,首先是多級繼承和多重繼承太複雜了,失去了抽象帶來的簡潔性。其次是父類與子類之間共享太多資訊,它們的耦合太緊密。三是父類與子類之間的關系在編譯時就靜态綁定了,很難做到在運作時多态。

  現在一般都提倡,隻繼承接口不繼承實作,通過組合達到代碼重用的目的。在《設計模式》中是這樣強調的,在MS的COM裡也是這樣做的。是以我基本上隻使用接口繼承,很少遇到什麼麻煩,建議大家也遵循這一準則。

繼續閱讀