何时使用继承

更新:2007 年 11 月

继承是非常有用的编程概念,但容易使用不当。接口通常更好用。本主题和 何时使用接口 可帮助您理解应在何时使用各种方法。

继承是好的选择,当:

  • 继承层次结构表示“属于”关系而不是“具有”关系。

  • 可以重用基类的代码。

  • 需要将相同的类和方法应用到不同的数据类型。

  • 类层次结构相当浅,而且其他开发人员不可能添加太多级别。

  • 需要通过更改基类对派生类进行全局更改。

我们将在下面依次讨论这些注意事项。

继承和“属于”关系

表示面向对象的编程中类关系的两种方法是“属于”和“具有”关系。在“属于”关系中,派生类显然是一种基类。例如,名为 PremierCustomer 的类表示与名为 Customer 的基类是“属于”关系,因为首要客户是客户之一。但是,名为 CustomerReferral 的类表示与 Customer 类之间是“具有”关系,因为客户引荐中含有客户,但客户引荐不是一种客户。

继承层次结构中的对象与其基类之间应具有“属于”关系,因为它们继承基类中定义的字段、属性、方法和事件。表示与其他类之间是“具有”关系的类不适合于继承层次结构,因为它们可能继承不适当的属性和方法。例如,如果 CustomerReferral 类是从前面讨论过的 Customer 类派生的,则它可能继承毫无意义的属性,如 ShippingPrefs 和 LastOrderPlaced。如这样的“具有”关系应使用不相关的类或接口表示。以下说明“属于”和“具有”关系。

“是一个”与“有一个”关系

基类和代码重用

使用继承的另一个原因是有代码重用的优点。设计良好的类可只调试一次,然后作为新类的基础反复使用。

一个常见的有效代码重用的示例与管理数据结构的库有关。例如,假设有一个管理几种内存中列表的大型业务应用程序。一个列表是客户数据库的内存中副本,是为了提高速度在会话开始时从数据库读入的。数据结构可能类似如下所示:

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

此处可以看到一种模式:两个列表行为相同(插入、删除和检索),但对不同的数据类型进行操作。维护两个代码库,使其基本执行同一功能是没有效果的。最有效的解决方案是将列表管理析解到其自己的类中,然后对不同的数据类型从该类继承:

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 类只需调试一次。然后可生成使用它的类,而永远不必再考虑列表管理。例如:

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

虽然基于继承的代码重用是功能强大的工具,但它也有关联的风险。即使最佳设计的系统有时也会以设计者未能预见的方式改变。对现有类层次结构的更改有时会带来意外的后果,在 部署后的基类设计更改 的“脆弱的基类问题”中会讨论一些这样的示例。

可互换的派生类

类层次结构中的派生类有时可与其基类互换使用,此过程称为基于继承的多态性。此方法结合了下面这两者:基于接口的多态性的最佳功能和选择重用或重写基类的代码。

绘图包中有一个示例,在该示例中此功能非常有用。例如,请考虑下面的代码片段,该代码片段不使用继承:

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

此方法引起一些问题。如果以后有人决定添加椭圆选项,将需要更改源代码;而您的目标用户可能甚至无法访问您的源代码。一个更微妙的问题是,绘制椭圆要求另一个与线不相关的参数(椭圆有一个大直径和一个小直径)。如果接着有人要添加折线(多条连接的线),则将添加另一个参数,而该参数与其他情况都不相关。

继承可解决这些问题中的大部分。设计良好的基类将特定方法的实现留给派生类,以便可适应任何种类的形状。其他开发人员可使用基类的文档在派生类中实现方法。其他类项(如 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

此示例显示如何通过在每一级别添加实现的详细信息,将通用用途的类转变为非常专用的类。

此时,可能需要重新评价派生类是否确实表示“属于”关系,或者是“具有”关系。如果新的矩形类只是线的组合,则继承不是最佳选择。但是,如果新的矩形是带有宽度属性的线,则保持该“属于”关系。

浅类层次结构

继承最适合于相对较浅的类层次结构。过深或过于复杂的类层次结构可能开发很困难。使用类层次结构的决策涉及权衡使用类层次结构和复杂性之间的优劣。通常来讲,应将层次结构限制在六级或更少级别。但是,任何特定类层次结构的最大深度取决于很多因素,包括每一级的复杂程度。

通过基类对派生类进行全局更改

继承的一个最强大的功能是能够在基类中进行更改,这些更改将传播到派生类中。仔细使用时,可更新单个方法的实现,从而使几十甚至上百个派生类都可使用该新代码。但是,这可能是危险的举动,因为这种更改可能导致其他人员设计的继承类出现问题。必须小心操作,以确保新基类与使用原始基类的其他类兼容。应特别避免更改基类成员的名称或类型。

例如,假设您设计一个基类,它带有 Integer 类型的字段以存储邮政编码信息,而其他开发人员已创建了使用继承的邮政编码字段的派生类。进一步假设您的邮政编码字段存储五位数字,而邮局扩展了邮政编码,增加了一个连字符和另外四位数字。在最糟糕的方案中,您可能修改基类中的该字段以存储 10 个字符的字符串,但其他开发人员将需要更改和重新编译派生类以使用这种新的大小和数据类型。

更改基类最安全的方法是只添加新成员。例如,在前面讨论的邮政编码示例中,可添加一个新字段来存储附加的四位数字。这样,可在不破坏现有应用程序的情况下,更新客户端应用程序以使用新字段。这种在继承层次结构中扩展基类的能力是接口所不具备的一个重要优点。

请参见

概念

何时使用接口

部署后的基类设计更改