天天看點

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

第1章和第2章講述了面向對象的基本概念。在開始學習關于建構面向對象系統的一些具體設計問題之前,我們需要更進一步了解面向對象的一些概念,比如構造函數、操作符重載以及多重繼承。我們也會講述錯誤處理技術以及面向對象的設計中作用域的重要性。

其中一些概念可能對深入了解面向對象設計并不是必需的,但設計和實作整個面向對象系統的人有必要了解。

構造函數對于結構化程式設計的程式員來說是個新概念。非面向對象的語言(比如cobol、c和basic)通常不會用到構造函數。c/c++中的結構體(struct)具有構造函數。前兩章提及過這個用于構造對象的特殊方法。在諸如java和c#之類的面向對象的語言中,構造函數名稱與類名相同。而visual basic .net使用關鍵字new,objective-c使用init關鍵字。這裡我們隻關注于構造函數的概念,而不會介紹所有語言的特殊文法。接下來用java代碼來實作一個構造函數。

例如,第2章中cabbie類的構造函數如下所示:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

編譯器會意識到這個方法名與類名完全相同,是以認為該方法是個構造函數。

小心

注意,java代碼(以及c#和c++)中,構造函數沒有傳回值。如果有傳回值,編譯器就不認為該方法是構造函數。

例如,如果類中有以下代碼,那麼編譯器不會認為該方法是構造函數,因為它有傳回值,這個傳回值是一個整數:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

該文法會導緻問題,因為雖然這份代碼可以通過編譯但得不到期望的行為。

當建立新對象時,首要事情之一是調用構造函數。請看以下代碼:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

new關鍵字建立了cabbie類的一個新執行個體,這會按需配置設定記憶體。然後會調用構造函數自身,并且可以通過參數清單傳遞參數。開發人員可以在構造函數内進行相應的初始化

工作。

是以,new cabbie()代碼将執行個體化一個cabbie對象,并調用cabbie方法,即該類的構造函數。

構造函數最重要的功能大概是當遇到new關鍵字時初始化記憶體配置設定。總之,構造函數中的代碼會把新建立的對象初始化到穩定、安全的狀态。

例如,如果有一個計數器(counter)對象,裡面有個屬性叫count,你需要在構造函數中将count設定為0:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

初始化屬性

在結構化程式設計中,名為housekeeping(管家)或initialization(初始化)的例程往往用于初始化目的。初始化屬性是構造函數經常執行的功能。

如果編寫了一個不包含構造函數的類,這個類仍然可以通過編譯,你也可以使用它。如果沒有為類提供一個顯式的構造函數,那麼類會有一個預設構造函數。請記住,無論你是否自定義了構造函數,類始終至少有一個構造函數。如果你沒有提供構造函數,系統會為你提供一個預設的構造函數。

除了建立對象本身之外,預設構造函數的另一個行為是調用父類的構造函數。大多數情況下,父類是語言架構的一部分,比如java中的object類。例如,如果沒有為cabbie類提供構造函數,系統會提供下面預設的構造函數:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

如果反編譯編譯器生成的位元組碼,你會看到這段代碼。這段代碼實際上是由編譯器插

入的。

在本例中,如果cabbie沒有顯式繼承自其他類,object類将會是它的父類。預設構造函數在有些場景下是适用的。然而,大多數場景下,需要自定義初始化一系列記憶體。不管在什麼情況下,在類中始終包含至少一個構造函數是一個優秀的實踐。如果類有屬性,最好始終在構造函數中初始化這些屬性。延伸開來,無論是否在編寫面向對象的代碼,初始化變量總是一個優秀的實踐。

提供構造函數

通用規則是即使并不需要在構造函數中做任何事情,也應當始終提供一個構造函數。你可以提供一個不包含任何代碼的構造函數,稍後再按需添加代碼。盡管使用編譯器預設提供的構造函數在技術上沒有任何問題,但基于文檔化和維護目的,這樣更容易看懂你的代碼。

這裡考慮維護問題并不奇怪。如果你使用的是預設的構造函數,後續操作添加了另一個構造函數,那麼系統不會再建立預設的構造函數。總之,隻有類中沒有包含任何構造函數時,系統才會添加預設的構造函數。一旦你提供了一個構造函數,系統就不再提供預設的構造函數。

大多數情況下,可以用多種方式建立對象。這需要提供多個構造函數。例如,請看count類:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

一方面,可以初始化屬性count為0,實作這一點很簡單,可以在一個構造函數中初始化count為0,如下所示:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

另一方面,可以傳遞一個初始化參數,進而可以設定count為其他數字:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

這叫作重載方法(重載适用于所有方法,不止是構造函數)。大部分的面向對象語言都提供了重載方法的功能。

1.?重載方法

重載可以讓程式員重複使用相同的方法名,隻要每次方法簽名不同即可。方法簽名包含了方法名以及參數清單(如圖3-1所示)。

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

是以,以下所有方法擁有不同的簽名:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

方法簽名可能包含傳回值類型,也可能不包含傳回值類型,這取決于不同的語言。在java和c#中,傳回值類型并不屬于簽名的一部分。例如,以下代碼即使傳回值類型不同,也不能通過編譯:

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

了解簽名最好的方式是編寫一些代碼然後進行編譯。

通過使用不同的簽名,你可以根據不同的構造函數來構造對象。如果你不能保證每次都能掌握足夠的資訊,那麼很适合這種方式。例如,當建立一個購物車時,顧客可能已經登入了自己的賬号(你會得到顧客所有的資訊)。而一個全新的顧客可能會向購物車中放入産品,但沒有任何賬号資訊,這樣的情況下構造函數初始化方式是不同的。

2.?使用uml對類模組化

我們再回頭看第2章中用到的資料庫閱讀器例子。構造資料庫閱讀器有兩種方式:

傳入資料庫名稱以及設定遊标在資料庫中的起始位置。

傳入資料庫名稱以及設定遊标在資料庫中的期望位置。

圖3-2展示了databasereader類的類圖。注意,該圖列出了此類的兩個構造函數。盡管該圖顯示了兩個構造函數,但并未包含參數清單,是以無法區分出這兩個構造函數。為了區分這兩個構造函數,可以檢視下面列出的database-reader類的對應代碼。

無傳回值類型

注意,在該類圖中,構造函數沒有傳回值類型。除了構造函數之外,其他所有方法必須要有傳回值類型。

以下代碼片段展示了該類的構造函數,以及構造函數如何初始化屬性(見圖3-3):

請注意在兩個場景中如何初始化startposition。如果沒有通過參數清單為構造函數提供位置資訊,那麼startpostion會被初始化為預設值,即0。

3.?如何構造父類

當使用繼承時,你必須知道如何構造父類。請記住,當使用繼承時,也繼承了父類的所有東西。是以必須熟悉父類的資料和行為。任何繼承的屬性都是完全可見的。然而,對構造函數的繼承則是不可見的。如果遇到了new關鍵字,那麼會配置設定對象,并發生以下步驟(見圖3-4):

  

《面向對象的思考過程(原書第4版)》一 第3章 進階的面向對象概念

1)在構造函數中會調用父類的構造函數。如果沒有顯式調用父類的構造函數,那麼系統會預設自動調用;不過可以在位元組碼中看到這段代碼。

2)對象中的所有屬性會被初始化。這些屬性是類中定義中的屬性(執行個體變量),不是構造函數或其他方法中的屬性(局部變量)。在databasereader代碼中,整數start-position是類的執行個體變量。

3)執行構造函數中的其餘代碼。

3.1.5 設計構造函數

我們已經看到了,設計類的一個最佳實踐是初始化所有屬性。有些語言中,編譯器會提供一部分初始化工作。與往常一樣,不要依賴編譯器來初始化屬性!在java中,隻有屬性被初始化後你才能使用它。如果屬性在代碼中很靠前,請確定你初始化屬性為一些有效值,比如設定整數為0。

構造函數用來確定應用程式處于穩定的狀态(我喜歡稱之為“安全”的狀态)。例如,如果把屬性作為除法運算中的分母,那麼初始化該屬性為0會導緻應用程式崩潰。你必須考慮除法中用0作為除數是非法操作。始終初始化屬性為0并不總是最好的方式。

在設計時,優秀的實踐應該是為所有屬性識别一個穩定的狀态,然後在構造函數中初始化這些屬性為穩定的狀态。