Share via


何時使用繼承

更新:2007 年 11 月

繼承是非常有用的程式撰寫觀念,但是很容易不當使用。通常有了介面會讓工作更順暢。本主題及 何時使用介面 可幫助您了解各種方法的使用時機。

在下列情況下非常適合選用繼承:

  • 您的繼承階層架構 (Inheritance Hierarchy) 代表 "is-a" 關聯性,而非 "has-a" 關聯性。

  • 您可從基底類別重複使用程式碼。

  • 您需要將相同類別及方法套用至不同的資料型別。

  • 類別階層架構是合理的表層 (Shallow),並且其他的開發人員不可能會加入許多更高層次。

  • 您想要藉由變更基底類別來對衍生類別進行全域變更。

下文中會依序討論這些考量。

繼承及 Is a 關聯性

物件導向程式設計中呈現類別 (Class) 關聯性 (Relationship) 的兩種方式是 "Is a" 及 "Has a" 關聯性。在 "is a" 關聯性中,衍生類別 (Derived Class) 顯然是一種基底類別。例如,名為 PremierCustomer 的類別與名為 Customer 的基底類別,代表的是 "is a" 關聯性,因為 "Premier Customer" 就是一個 "Customer"。然而,名為 CustomerReferral 的類別與 Customer 的類別,代表的則是 "has a" 關聯性,因為 "Customer Referral" 會有一個 "Customer",但是 "Customer Referral" 不是一種 "Customer"。

因為繼承階層架構中的物件繼承基底類別中所定義的欄位、屬性、方法及事件,所以物件應具有與其基底類別的 "Is a" 關聯性。代表與其他類別 "Has a" 關聯性的類別不適合繼承階層架構,因為這些類別會繼承不適當的屬性及方法。例如,如果 CustomerReferral 類別衍生自之前討論的 Customer 類別,則會繼承如 ShippingPrefs 和 LastOrderPlaced 之類無意義的屬性。應使用沒有關聯的類別或介面來代表此類的 "Has a" 關聯性。下圖分別顯示 "Is a" 和 "Has a" 關聯性的範例。

"Is a" 和 "Has a" 的關聯性

基底類別及重複使用程式碼

使用繼承的另一項原因是具有程式碼重複使用的優點。設計完善的類別可以一次偵錯,然後做為新類別的基礎以重複使用。

高效率程式碼重複使用的常見範例,是結合管理資料結構的程式庫。例如,假定您具有管理數種記憶體內部清單的大型商務應用程式。其中一項是客戶資料庫的記憶體內部複本,這是在工作階段 (Session) 開始時讀入至記憶體中以便加速存取。資料結構看起來有些類似於下面顯示的範例:

Class CustomerInfo
    Protected PreviousCustomer As CustomerInfo
    Protected NextCustomer As CustomerInfo
    Public ID As Integer
    Public FullName As String

    Public Sub InsertCustomer(ByVal FullName As String)
        ' Insert code to add a CustomerInfo item to the list.
    End Sub

    Public Sub DeleteCustomer()
        ' Insert code to remove a CustomerInfo item from the list.
    End Sub

    Public Function GetNextCustomer() As CustomerInfo
        ' Insert code to get the next CustomerInfo item from the list.
        Return NextCustomer
    End Function

    Public Function GetPrevCustomer() As CustomerInfo
        'Insert code to get the previous CustomerInfo item from the list.
        Return PreviousCustomer
    End Function
End Class

您的應用程式也可能具有使用者已加入至購物清單的類似產品清單,下列程式碼片段顯示此項範例:

Class ShoppingCartItem
    Protected PreviousItem As ShoppingCartItem
    Protected NextItem As ShoppingCartItem
    Public ProductCode As Integer
    Public Function GetNextItem() As ShoppingCartItem
        ' Insert code to get the next ShoppingCartItem from the list.
        Return NextItem
    End Function
End Class

您可在本範例中看到一種模式:兩個清單以相同方式運作 (插入、刪除及擷取),但是處理不同的資料型別。維護兩個程式碼基底 (Code Base) 來執行本質上的相同函式,是沒有效率的。最有效率的方案是將清單管理分解成自己的類別,然後針對不同的資料型別以繼承自該類別:

Class ListItem
    Protected PreviousItem As ListItem
    Protected NextItem As ListItem
    Public Function GetNextItem() As ListItem
        ' Insert code to get the next item in the list.
        Return NextItem
    End Function
    Public Sub InsertNextItem()
        ' Insert code to add a item to the list.
    End Sub

    Public Sub DeleteNextItem()
        ' Insert code to remove a item from the list.
    End Sub

    Public Function GetPrevItem() As ListItem
        'Insert code to get the previous item from the list.
        Return PreviousItem
    End Function
End Class

ListItem 類別只需要經過一次偵錯。然後您可建置 (Build) 使用 類別的類別,而不需要總是一再思索清單管理。例如:

Class CustomerInfo
    Inherits ListItem
    Public ID As Integer
    Public FullName As String
End Class
Class ShoppingCartItem
    Inherits ListItem
    Public ProductCode As Integer
End Class

雖然重複使用繼承架構程式碼是功能強大的工具,但是還是存在有相關的風險。甚至最佳設計的系統有時候也會以設計工具無法預見的方式變更。有時候,變更現有的類別階層會產生非預期結果;在部署後的基底類別設計變更中的<損壞的基底類別問題>內,將會討論一些範例。

可交換的衍生類別

有時候可使用類別階層架構中的衍生類別與其基底類別交換,這是一種稱為繼承架構多型的處理序。此項做法伴隨著重複使用或覆寫來自於基底類別的程式碼選項,組合介面架構多型的最佳功能。

例如,在繪圖套件 (Package) 中,此項做法可能非常有用。例如,參考下列不使用繼承的程式碼片段:

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
    ByVal Y As Integer, ByVal Size As Integer)

    Select Case Shape.type
        Case shpCircle
            ' Insert circle drawing code here.
        Case shpLine
            ' Insert line drawing code here.
    End Select
End Sub

此項做法會引發某些問題。如果其他人決定之後加入橢圓選項,則必須變更來源程式碼;您的目標使用者甚至可能沒有來源程式碼的存取權。更微妙的問題在於,繪製橢圓需要另一個參數 (橢圓具有長軸及短軸),而這項參數與線條案例無關。然後,如果其他人想要加入折線 (多重連接線),則會加入另一個參數,而這項參數與其他案例無關。

繼承解決了這些大部分的問題。設計完善的基底類別會保持指定方法的實作直到衍生類別,所以可適應各種形狀。其他的開發人員可藉由使用基底類別的文件來實作衍生類別中的方法。因為所有的子代 (Descendant) 都會使用其他的類別項目 (諸如 x 及 y 座標),所以可將這些類別項目建置成基底類別。例如,Draw 可能是 MustOverride 方法:

MustInherit Class Shape
    Public X As Integer
    Public Y As Integer
    MustOverride Sub Draw()
End Class

然後,您可加入至該類別,使之適合於不同的形狀。例如,Line 類別只可能需要 Length 欄位:

Class Line
    Inherits Shape
    Public Length As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for this shape.
    End Sub
End Class

因為沒有您的來源程式碼存取權的其他開發人員可視需要用新的衍生類別來擴充您的基底類別,所以此項做法非常有用。例如,名為 Rectangle 的類別可衍生自 Line 類別:

Class Rectangle
    Inherits Line
    Public Width As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for the Rectangle shape.
    End Sub
End Class

本範例呈現出您如何可藉由在每層層級上加入實作詳細資料,從一般用途類別移動至特定類別。

此時重新評估衍生類別是否真的表示 "is a" 關聯性,還是 "has a" 關聯性,可能比較恰當。如果新矩形類別僅僅是由線條所組成,則繼承並非是最佳選擇。然而,如果新矩形是具有寬度屬性的線條,則會維持 "Is a" 關聯性。

表層類別階層架構

繼承最適用於相對的表層類別階層架構。極度深層及複雜的類別階層架構可能難以開發。使用類別階層架構的決定涉及依據複雜性來權衡使用類別階層架構的優勢。按照一般規則,您應將階層架構限制在六層或更少。然而,任何特定類別階層架構的最大深度值取決於一些要素,包括每個層次上的複雜性總數。

透過基底類別全面變更衍生類別

繼承中一項最強的功能是能夠變更傳播到衍生類別的基底類別。當謹慎使用時,您可以更新單一方法的實作,以及數十個甚至數百個衍生類別都可以使用新的程式碼。然而,因為此類的變更會導致其他人所設計的繼承類別上的問題,所以這可能是危險的方式。一定要謹慎處理,以確保新的基底類別相容於使用原始類別的類別。您應特別避免變更基底類別成員的名稱或型別。

例如,假定您設計的基底類別具有儲存郵遞區號資訊的 Integer 型別欄位,並且其他的開發人員已建立使用繼承郵遞區號欄位的衍生類別。進一步假設您的郵遞區號欄位會儲存 5 位數字,並且郵局已使用短破折號及額外的 4 位數字來擴充郵遞區號。在最壞情況下,您可修改基底類別中的欄位以儲存 10 個字元的字串,但是其他開發人員需要變更及重新編譯衍生類別,才能使用新大小及資料型別。

變更基底類別的最安全方法是直接加入新成員。例如,您可加入新欄位,以儲存之前討論的郵遞區號範例中的額外 4 位數字。以此方式可更新用戶端應用程式以使用新欄位,而不需要破壞現有的應用程式。繼承階層架構中擴充基底類別的功能是介面所沒有的重要優點。

請參閱

概念

何時使用介面

部署後的基底類別設計變更