準則: 一般化
一般化是指在不同類別之間擷取共用內容的類別關係。這個準則示範如何使用這個關係。
關係
主要說明

一般化

現實生活中的許多東西都有共用內容。例如,小狗和小貓都是動物。物件也可以有共用內容,您可以在它們的類別之間,利用一般化來釐清這些內容。您可以將共用內容擷取到它們自己的類別中,以便未來能更容易變更和維護系統。

一般化顯示一個類別繼承另一個類別。繼承的類別稱為後代。被繼承的類別稱為上代。繼承關係表示上代的定義對於後代的物件也有效,其中包括如屬性、關係或物件作業之類的任何內容。一般化的繪製,是從後代類別畫向上代類別。

一般化可以在許多階段中進行,讓您建立複雜而多層的繼承階層模型。一般內容放在繼承階層的上層部分,特殊內容的位置較低。換句話說,您可以利用一般化來建立一般概念的特殊化模型。

範例

在回收機系統中,所有類別 (Can、Bottle 和 Crate) 分別描述不同類型的存放項目。除了類型相同之外,它們有兩個共用內容:高度和重量。您可以利用獨立類別 Deposit Item 中的屬性和作業來建立這些內容的模型。Can、Bottle 和 Crate 都繼承這個類別的內容。

圖解說明詳見隨附的文字。

Can、Bottle 和 Crate 等類別都有高度和重量這兩個共用內容。每個都是 Deposit Item 這個一般概念的特殊化。

多重繼承

類別可以利用多親繼承來繼承多個其他類別,不過,它通常只會繼承一個類別。

如果您使用多親繼承,有些潛在的問題不可不知:

  • 如果類別繼承多個類別,您必須檢查各個上代如何指定關係、作業和屬性的名稱。如果在多個上代中出現相同的名稱,您必須描述這對於特定繼承類別的意義,例如,將名稱限定為指示它的宣告來源。
  • 如果使用重複的繼承關係;在這個情況下,後代會多次繼承同一個上代。當發生這個情況,繼承階層會有如下所示的「菱形」。

圖解說明詳見隨附的文字。

多重和重複的繼承。Scrolling Window With Dialog Box 類別多次繼承 Window 類別。

在這個環境定義中,可能會出現下列問題:「Scrolling Window With Dialog Box 的實例包括幾份 Windows 的屬性?」 因此,如果您在使用重複的繼承,您必須有它的語意的清晰定義;在大部分情況下,這是由支援多親繼承的程式語言來定義。

一般而言,支配多親繼承的程式語言規則都很複雜,通常不容易正確使用。因此,建議您必要時才使用多親繼承,且要始終小心。

抽象和具體類別

未實例化的類別,如果它的存在只是為了供其他類別繼承,它就是抽象類別。實際產生實例的類別則是具體類別。請注意,抽象類別必須有至少一個後代,它才有用。

範例

倉庫系統中的 Pallet Place 是代表不同集裝架位置類型之共用內容的抽象實體類別。Station、Transporter 和 Storage Unit 等具體類別繼承這個類別,它們都可以作為倉庫中的集裝架位置。所有這些物件都有一個共用內容:它們可以存放一或多個集裝架。

圖解說明詳見隨附的文字。

被繼承的類別(這裡是 Pallet Place)是抽象類別,它本身並沒有實例化。

使用

由於類別模板有不同用途,因此,不同類別模板之間的繼承關係並沒有意義。例如,讓界限類別繼承實體類別,會使界限類別成為混合物。因此,您只應在相同模板的類別之間使用一般化。

您可以利用一般化來表示類別之間的兩種關係:

  • 子類型化,指定後代是上代的子類型。子類型化表示後代繼承上代的結構和行為,且後代是上代的一個類型(也就是說,後代是一個子類型,可以在任何狀況之下替代它的所有上代)。
  • 子類別化,指定後代是上代的子類別(不是子類型)。子類別化表示後代繼承上代的結構和行為,且後代不是上代的一個類型。

您可以將多個類別共用的內容分開,將它們放在獨立的類別中,供其他類別繼承,或建立新的類別,將較一般的類別特殊化,並讓它們繼承一般類別,來建立這類關係。

如果兩個變式相符,在設定類別之間的正確繼承關係時,應該不會有困難。不過,在某些情況下,它們會不符,您必須使繼承的使用維持可理解的情況。最起碼,您應該知道模型中每個繼承關係的目的。

支援多型性的繼承

子類型化表示後代是一個子類型,可以在任何狀況之下替代它的所有上代。 子類型化是多型性的特殊案例,它是一個很重要的內容,因為它可讓您設計所有用戶端(使用上代的物件),而不需要考量上代的潛在後代。這會使用戶端物件更具一般性,更可重複使用。當用戶端使用實際的物件時,它會以一種特定方式來運作,且一律會發現物件在執行它的作業。子類型化可確保系統可容忍子類型集的變更。

範例

在倉庫系統中,Transporter Interface 類別定義了與所有類型的運輸設備(如起重機和卡車)通訊的基本功能。這個類別定義了 executeTransport 作業及其他東西。

圖解說明詳見隨附的文字。

Truck Interface 和 Crane Interface 類別都繼承 Transporter Interface;也就是說,這兩個類別的物件都會回應 executeTransport 訊息。這些物件隨時都可能代表 Transporter Interface,且會提供它的所有行為。因此,其他物件(用戶端物件)可以傳送訊息給 Transporter Interface 物件,但不需要知道 Truck Interface 或 Crane Interface object 物件會不會回應訊息。

Transporter Interface 類別甚至可以是抽象的,絕不獨立建立實例。在這個情況下,Transporter Interface 可能只定義 executeTransport 作業的簽章,而由後代類別來實作它。

部分物件導向語言(如 C++)會使用類別階層作為類型階層,迫使設計者利用繼承來建立設計模型中的子類型。其他物件導向語言(如 Smalltalk-80)則不會在編譯時進行類型檢查。如果物件無法回應收到的訊息,它們會產生錯誤訊息。

甚至在不含類型檢查的語言中,利用一般化來指示子類型關係也很好。在某些情況下,不論語言是否接受,您都應該利用一般化來使物件模型和程式碼更容易瞭解和維護。如此使用繼承關係是不是好的樣式,會隨著程式語言的慣例而大不相同。

支援重複使用實作的繼承

子類別化構成了一般化重複使用的方面。當子類別化時,您要考慮繼承其他類別所定義的內容,可以重複使用實作的哪些部分。當您實作特定類別時,子類別化可以節省工作,讓您重複使用程式碼。

範例

在 Smalltalk-80 類別庫中,Dictionary 類別繼承 Set 的內容。

圖解說明詳見隨附的文字。

這個一般化的原因在於 Dictionary 後來可以重複使用 Set 實作中的某些一般方法和儲存策略。雖然您可以將 Dictionary 視為一個 Set(包含鍵值組),但 Dictionary 並不是 Set 的子類型,因為您不能在 Dictionary 中加入任何種類的物件(只有鍵值組)。使用 Dictionary 的物件並不知道它實際上是一個 Set。

子類別化通常會造成很難理解、很難維護的不合邏輯的繼承階層。因此,除非在使用程式語言方面有其他建議,否則,不建議您只為了重複使用而使用繼承。這類重複使用的維護,通常會很難處理。Set 類別中的任何變更都可能隱含了所有繼承 Set 類別之類別的大量變更。請注意這一點,只繼承穩定的類別。繼承會實際凍結 Set 的實作,因為它的變更成本太高。

程式語言中的繼承關係

當在設計中使用一般化關係時,應該相當依賴程式語言的繼承語意和所提出的繼承用法。物件導向語言支援類別之間的繼承,但非物件導向語言不支援。您應該在設計模型中處理語言特性。如果您使用的語言不支援繼承或多親繼承,您必須在實作中模擬繼承關係。在這個情況下,您最好在設計模型中建立模擬的模型,而不用一般化來說明繼承結構。利用一般化來建立繼承結構的模型,之後,在實作中模擬繼承,可能會毀壞設計。

如果您使用的語言不支援繼承或多親繼承,您必須在實作中模擬繼承關係。在這個情況下,您最好在設計模型中建立模擬的模型,而不用一般化來說明繼承結構。利用一般化來建立繼承結構的模型,之後,只在實作中模擬繼承,可能會毀壞設計。

在模擬期間,您可能必須變更介面及其他物件內容。建議您利用下列其中一種方式來模擬繼承關係:

  1. 讓後代將訊息轉遞給上代。
  2. 將上代的程式碼複製到每個後代中。在這個情況下,不會建立任何上代類別。

範例

在這個範例中,後代會利用鏈結(關聯實例)將訊息轉遞給上代。

圖解說明詳見隨附的文字。

Can、Bottle 和 Crate 物件的共用行為指派給一個特殊類別。在必要時,共用這個行為的物件會傳送一則訊息給 Deposit Item 物件來執行行為。