实现继承层次结构比设计继承层次结构容易,这使得在明确确定您的需要之前开始编码是冒险的事情。实现后纠正类层次结构中的设计错误可能需要进行会禁用现有应用程序的代码更改。本节讨论继承层次结构设计注意事项,指出可以帮助避免犯这样错误的信息。
本节内容
-
类层次结构设计的可扩展性注意事项
-
讨论如何设计类层次结构,以便其他开发人员可更新或扩展它们。
-
选择方法访问级别的注意事项
-
描述类内可访问级别的正确使用。
-
部署后的基类设计更改
-
解释开发人员尝试对类层次结构进行更改时面临的问题。
相关章节
即使设计良好的类层次结构随着时间的推移也需要不断发展。在设计类层次结构时提前做出选择可简化以后的工作。
扩展类层次结构
下面的列表包含一些使类层次结构更容易扩展的建议:
-
从通用到专用定义类层次结构。将继承层次结构的每一级别的类定义得尽可能通用。派生类可继承、重用和扩展基类中的方法。
例如,假设正在设计一个模拟计算机硬件的类层次结构。当开始建立输出设备模型时,可定义名为 Display、Printer 和 File 的类。然后可以定义实现这些基类中所定义的方法的类。例如,LCDDisplay 类可从 Display 派生并实现一个名为 EnterPowerSaveMode 的方法。
-
在定义数据类型和存储区时要慷慨,以避免以后更改困难。例如,可能考虑使用 Long 类型的变量,即使当前数据可能仅需要标准的 Integer 变量。
-
只公开派生类需要的项。Private 字段和方法可减少命名冲突并防止其他用户使用您以后可能需要更改的项。
-
只应将派生类需要的成员标记为 Protected。这可确保只有派生类依赖这些成员,从而在开发期间可以更容易更新这些成员。
-
确保基类方法不依赖 Overridable 成员(它们的功能可能会被继承类更改)。
继承是非常有用的编程概念,但容易使用不当。接口通常更好用。本主题和何时使用接口可帮助您理解应在何时使用各种方法。
继承是好的选择,当:
-
继承层次结构表示“属于”关系而不是“具有”关系。
-
可以重用基类的代码。
-
需要将相同的类和方法应用到不同的数据类型。
-
类层次结构相当浅,而且其他开发人员不可能添加太多级别。
-
需要通过更改基类对派生类进行全局更改。
我们将在下面依次讨论这些注意事项。
继承和“属于”关系
表示面向对象的编程中类关系的两种方法是“属于”和“具有”关系。在“属于”关系中,派生类显然是一种基类。例如,名为 PremierCustomer 的类表示与名为 Customer 的基类是“属于”关系,因为首要客户是客户之一。但是,名为 CustomerReferral 的类表示与 Customer 类之间是“具有”关系,因为客户引荐中含有客户,但客户引荐不是一种客户。
继承层次结构中的对象与其基类之间应具有“属于”关系,因为它们继承基类中定义的字段、属性、方法和事件。表示与其他类之间是“具有”关系的类不适合于继承层次结构,因为它们可能继承不适当的属性和方法。例如,如果 CustomerReferral 类是从前面讨论过的 Customer 类派生的,则它可能继承毫无意义的属性,如 ShippingPrefs 和 LastOrderPlaced。如这样的“具有”关系应使用不相关的类或接口表示。以下说明“属于”和“具有”关系。
![“Is a”和“Has a”关系](https://i-blog.csdnimg.cn/blog_migrate/7db8afbc9e9bd94acd517f0b854ab501.png)
基类和代码重用
使用继承的另一个原因是有代码重用的优点。设计良好的类可只调试一次,然后作为新类的基础反复使用。
一个常见的有效代码重用的示例与管理数据结构的库有关。例如,假设有一个管理几种内存中列表的大型业务应用程序。一个列表是客户数据库的内存中副本,是为了提高速度在会话开始时从数据库读入的。数据结构可能类似如下所示:
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 个字符的字符串,但其他开发人员将需要更改和重新编译派生类以使用这种新的大小和数据类型。
更改基类最安全的方法是只添加新成员。例如,在前面讨论的邮政编码示例中,可添加一个新字段来存储附加的四位数字。这样,可在不破坏现有应用程序的情况下,更新客户端应用程序以使用新字段。这种在继承层次结构中扩展基类的能力是接口所不具备的一个重要优点。
接口允许您将对象的定义与实现分开,因而是一种功能强大的编程工具。接口继承和类继承各有优缺点,您最终可能会在项目中将二者结合使用。此页和何时使用继承将帮助您确定哪种方法最适合于您的情况。
实现的灵活性
以下是为何使用接口继承而不用类继承的一些其他原因:
-
在应用程序要求很多可能不相关的对象类型以提供某种功能的情况下,接口的适用性更强。
-
接口比基类更灵活,因为可以定义单个实现来实现多个接口。
-
在无需从基类继承实现的情况下,接口更好。
-
在无法使用类继承的情况下接口是很有用的。例如,结构无法从类继承,但它们可以实现接口。
-
部署后的基类设计更改
理想情况下,类层次结构部署后永不再更改,因为即使很小的更改都有产生意外后果的危险。在真实世界里,有时这种更改是无法避免的:产品要求可能会更改,而且有时设计规范会遗漏关键元素。有一类继承问题称为“脆弱的基类问题”。
脆弱的基类问题
使用继承层次结构的主要缺点是一种称为“脆弱的基类”的问题。对基类的更改经常要求更改、重新编译和重新分发基类以及派生类中的所有代码。当多个开发人员创作基类和派生类时,此问题甚至更加困难,这是因为每一方都可能拒绝对他们的源代码的访问。最糟糕的情形是,客户可能无意中将已编译的二进制形式的更新基类与原始的不兼容二进制版本的派生类一起使用。
可能会破坏派生类的基类更改包括重写或更改基类成员的数据类型。
尽量避免脆弱的基类问题
避免脆弱的基类问题的最佳方法是仅在派生类中进行更改。但是,这种方法不是始终可行的,因为这要求您在第一次发布基类时要考虑到各种可能性;不管一个人如何努力,有时还是无法避免对基类进行不可预见的更改。
使用 MustInherit 类和 MustOverride 方法可以帮助减少脆弱的基类问题,因为这样做将实现的详细信息转移到了派生类中,从而减少了对更改基类的需要。但是,即使进行了最佳规划,此方法也不是始终可行的。
派生类中的阴影数据成员也是有帮助的,因为它们减少了与基类成员的命名冲突。
扩展基类功能的最安全的方法是添加新成员。新成员可能破坏基类的唯一方式是:派生类使用 Overloads 关键字从基类继承方法,并引入具有相同名称的新方法。可通过在派生类中显式指定要从基继承的基类方法来避免此问题,指定的方法是重新定义这些方法并对它们进行委托。
在某种意义上,所有基类在某种程度上都是脆弱的。总而言之,脆弱的基类问题无法消除,但可以通过仔细设计类层次结构而减少基类更改的需要,以及在这种更改不可避免时进行彻底的测试来尽最大可能避免该问题。