实现继承
实现继承也称子类化、代码继承或类继承,要求在子类中组合父类的特性,必要时允许新的实现来重写它们。
实现继承允许共享特性描述、代码复用以及多态性。
接口继承的使用是安全的,因为它只涉及契约部分的继承即操作型构。
实现继承涉及代码的继承,即实现部分的继承。
如果不注意控制和限制,实现继承将会弊大于利。[1]
继承可能存在的弊端有[2]:
- 脆弱的基类问题:对基类的修改会影响到它所有的子孙类。
- 臃肿的子类问题:如果“尽可能地继承”而非“适时地继承”,子类可能会越来越臃肿庞大。
- 可能由不恰当使用带来不必要的复杂性:面向对象的语言支持各种构造,使用不当会引入许多不必要的复杂性。
扩展继承[$√$]
继承的唯一恰当使用就是将继承作为类的增量式定义。
子类具有比父类更多的特性(属性和/或方法),子类是父类的一种,这就是扩展继承。
在扩展继承中,特性的重写要谨慎使用,应该只允许使特性更特殊化(如限制值的范围或使操作的实现更高效),而不改变特性的含义。如果重写改变了特性的含义,则子类对象就不能再替换父类对象了。[1]
限制继承[$×$]
在扩展继承中,用新的特性扩展子类的定义。然而,有一些继承来的特性在子类中被禁止(被重写),因此使用继承作为一种限制机制也是可能的,这样的继承被称为限制继承。
限制继承是有问题的。
从泛化的观点看,子类没有包括父类的所有特性。倘若使用对象的人知道被重写(禁止)的特性的话,父类对象仍然能够被子类对象所替换。
在限制继承中,一个类的特性通过继承被用于实现另一个类。如果重写未做扩展,限制继承能够带来益处。但一般来说,限制继承会带来维护上的问题。通过将继承来的方法实现为空,即什么也不做,限制继承将可能完全禁止继承来的问题。[1]
方便继承[$×$]
在系统建模中,既不是扩展继承也不是限制继承的继承是不好的。当两个或多个类具有相似的实现,但在这些类所表示的概念之间没有分类关系的时候,会出现这种继承。
任意选择一个类作为其他类的父类,这样的继承被称为方便继承。
方便继承是不恰当的,它在语义上不正确,导致了扩展式重写。
由于对象不再属于相似的类型,可替换性原则就无效了。[1]
继承与封装
封装要求只能通过对象接口中的操作才能访问到对象的状态。如果强迫封装,它将带来高度的数据独立性,这样所封装的数据结构将来的变化就不会导致一定要修改已有的程序。
关于继承与封装,尽管它们同为面向对象的三大特性,但现实情况是,封装与继承和查询能力是正交的,它不见得与这两个特性一起折中考虑。
事实上,将所有的数据声明为private也是不现实的。
继承允许子类直接访问protected属性,它削弱了封装。当计算涉及不同类的对象时,可能要求这些不同的类彼此是friend的或者让元素具有包可见性,这就进一步破坏了封装。
封装是针对类的概念,不针对于对象。
事实上,大多数的OO程序设计环境中,一个对象不能对同一个类的另一个对象隐藏任何东西。
用户基于SQL访问数据库中的数据,期望在查询时直接查询属性,而不是被迫使用某些数据访问方法,否则会导致查询的表达更困难、更易出错。
设计应用时,应该使它们达到期望的封装水平,但还要与继承、数据库查询以及计算需求相权衡。[3]
继承与泛化
泛化是通过通用类(超类或父类)与专用类(子类)之间的一种语义关系,子类是父类的一种,子类是父类的特殊化,子类对象可以用在允许使用父类的场合,任何子类实例是父类的非直接实例。
通过泛化,可以不必陈述已经定义的属性,父类中定义的属性和方法可以在子类中复用,我们称子类继承了父类的属性和方法。
泛化是很强大的软件复用技术,极大地简化了模型的语义和图形表示。
泛化有助于增加规格说明、类之间公共属性的利用以及更好地确认变更的位置。
泛化关系在UML中用指向其父类的空心三角表示。
泛化是一个强有力的实用概念,但由于复杂的继承机制,它也可能带来很多问题,特别是在大型软件项目中。
必须明确的是,继承$≠$泛化:
- 继承只用于类而非对象,只用于类型而非值。
- 泛化是一种类之间的语义关系,说明子类接口必须包含父类的所有特性;继承是一种机制,通过这种机制,较特殊的元素可以合并较一般的元素中定义的结构和行为。[4]
Reference
[1]实现继承的正确实践和不当实践
[2]继承和多态的弊端
[3]继承与封装
[4]继承与泛化