设计: 为什么要慎用继承?
如果你是个不喜欢看细节的人,这篇文章主要说两点:
慎用继承
即使要继承,也请尽量让子类仅依赖父类的公有接口,而不要依赖父类的具体实现
正文
面向对象设计里有一个原则叫组合复用原则(Composition/Aggregate Reuse Principle, CARP),是说尽量使用对象组合,而不是继承来达到复用的目的。
从语言支持来看,C++支持继承多重继承,Java只支持单继承,Golang不支持继承。
继承好像很不被待见。
但在日常中工作,还是经常看到滥用继承的情况,造成了不必要的复杂性和不灵活。本文打算说说我看到的继承设计的问题。
复用
据我观察,大多数BUG都来自不合理的复用,分为两种情况:
该复用的没有复用(没有做到DRY): ”呀,只改了这几处,还有一处忘记改了"
不该复用的复用了(或者说以不合理的方式复用了): “噫,我只改了这处,没想到那里出问题了”
继承和组合是复用的两种最基本的方式。可以从三方面理解复用的好处:
方便理解,复用一般是复用已经封装好的逻辑,只要理解逻辑的输入输出,不用理解逻辑的实现
方便维护,一个改动,只用改一处,而不用改多处
保持灵活性,只要接口稳定,可以灵活调整复用逻辑的实现
理由一: 继承更不好理解
继承的复用是隐式的,组合是显示的。继承不仅复用逻辑,还复用状态。组合一般会给复用的对象起个名字,有助于理解。对于支持多重继承的语言来说,如果设计成菱形继承关系,就更坑了。
理由二: 继承容易滥用,破坏封装
和组合相比,继承容易被滥用为“白盒”复用,破坏封装性。
来看个具体的例子(用python描述):
从这个例子中可以看到几个问题:
父类和子类的实现相互耦合,无法脱离父类单独理解子类(不仅要理解父类的公有接口,还要理解父类的具体实现)。而且,整个public_func的实现在父类和子类间跳来跳去(public_func -> _impl_func -> _support_func),增加理解难度,这还只是一层继承的情况,想想如果有多层继承,状态(如_param)和逻辑(如_pubic_func)分散在各个层级,需要理解每一层的实现,并且要在各层间跳来跳去,会多么复杂!
_support_func和_param是父类的具体实现,由于子类的存在,这些实现不能随意调整,导致不够灵活。支持protected的语言稍微好点,但protected的字段和方法也是不能随意调整的,除了表达的设计意图和public有区别,应该和public的成员一样对待。
理由三: 继承更不灵活
除了被子类依赖的实现不能随意调整,继承不够灵活还体现在以下几方面:
继承关系是静态的,应用不能在运行时,根据一些条件,动态更换父类
继承关系是静态层级结构,不能任意组合
继承复用的粒度太粗,all or nothing,组合可以只复用需要复用部分的逻辑
怎么合理使用继承?
继承是面向对象设计的三大特性之一,只是其太容易被滥用,导致很不受待见。不过,我们还是在很多设计里看到其身影,特别是客户端相关库的设计,比如Java Swing的JComponent系列,Android的View系列,iOS的UIView系列等,Android的Activity机制也是通过继承来定义生命周期管理的,这些设计有共同的特点:
极大限制了子类对父类实现的耦合,只通过良好定义的机制扩展,子类基本只依赖父类的公有接口
每一层父类,都是一个完备的抽象,经过良好的封装,基本可以独立理解
事实上,虽然我们很多时候继承这些类,但我们基本不用关心这些类的实现。在我们自己进行继承设计时,遵从这些约束,也会产出更高质量的设计。
欢迎通过评论分享你的观点,进行更多讨论 :)