跳到主要内容

继承:先用,坏了再换成委托

1. 这一章讲什么

原书名录的最后一章处理面向对象最著名的特性。开篇的定调没有一丝吹捧:「继承机制十分实用,却也经常被误用,而且常得等你用上一段时间,遇见了痛点,才能察觉误用所在」1。所以本章的手法天然分两半:前半是正向经营(共性上提、差异下沉、修剪掉没用的类),后半是故障处理(继承用错了地方时,怎么安全地换成委托)。我们的章节次序按这条主线走,末尾用第 09 章的鸟例子做一次「两次变身」的对照走查。

2. 顶层全景

正向经营 故障处理
子类里发现重复 ──→ 函数/字段上移 判据① 一套继承只能表达一个变化方向
多个类做相似的事 ──→ 提炼超类 判据② 超类修改会波及子类(耦合过紧)
类型码多处分支 ──→ 以子类取代类型码 判据③ 超类函数子类用不上 / 实例不是超类实例
子类不再值得 ──→ 移除子类/折叠 │

以委托取代子类 / 以委托取代超类

图说:左边让继承变好,右边承认继承会错;三道判据是两边的分界线。

3. 共性上提:上移家族与提炼超类

重复是上移的第一动机:「重复的两个函数现在也许能够正常工作,但假以时日却只会成为滋生bug的温床」——风险是「修改其中一个却未能修改另一个」2。子类函数体一字不差时最显然;有趣的是观察差异的价值:「它们经常会向我展示那些我忘记测试的行为」3。典型操作路径:两个子类各有一个干同样事的函数,先用函数参数化把签名调一致,再复制到超类、逐个删除子类版本。JavaScript 是动态语言,超类函数调用子类实现不会报错,但作者会留一个「陷阱(trap)函数」——超类方法直接抛「子类责任」错误,给未来的子类作者指路4

提炼超类处理跨类重复:两个类做相似的事(都有名字、都有年度成本),就把共同元素提到超类。这一节最有价值的是对「先设计后继承」的反驳:真实世界的分类可以作参考,但「还有很多时候,合理的继承关系是在程序演化的过程中才浮现出来的」5。与「提炼类」(拆出一个组件)之间怎么选?「其实就是继承和委托之间的选择」;作者首选提炼超类,理由坦率:「即便选错了,也总有以委托取代超类这瓶后悔药可吃」6

4. 差异下沉:类型码变子类

软件总要表现「相似但又不同的东西」:员工按职位分(工程师/经理/销售),订单按优先级分。第一种工具是类型码字段——一个取值如枚举(把所有可能值一一列出的固定集合)/字符串/数字的字段,值常常来自外部服务7。大多数时候类型码就够了;往前一步引入子类的理由有两个,作者叫「两个诱人之处」:一是多态处理条件逻辑(几个函数都按类型码分支时,第 09 章的多态手法接上);二是类型特有字段有了家——「“销售目标”只对“销售”这类员工才有意义」,放进 Salesman 子类,比加运行时校验清楚得多8

设计决策只有一个岔口:直接让「工程师继承员工」,还是把类型码包装成「职位」对象、让「工程师继承职位」?判据也简单:直接继承方便,但职位类别没法复用到别的场合;「如果员工的类别是可变的,那么也不能使用直接继承的方案」——员工转岗了,他的类不能换,他的字段可以换9

5. 修剪:移除子类

继承体系也要会删。「子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了」——把子类折叠回超类的一个类型字段10。原书例子几乎是个玩笑:Person 下有 Male/Female 子类各返回性别代码,换成超类一个字段即可。子类消失的常见原因:它支持的变化被搬走了,或者「为了应对未来的功能」建了,那个功能却没来。

6. 三道判据与两条退路

正向经营讲完,进入本章真正的重头戏:什么时候继承本身是错的。

判据①:一套继承只能表达一个方向的变化。 「继承这张牌只能打一次」——人可以按年龄段分(年轻人/老人),也可以按收入分(富人/穷人),但不能同时用两种继承11

判据②:继承的耦合是硬耦合。 「继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类」;两个类若分属不同模块、不同团队,更要命12

判据③:语义不匹配。 经典反面教材是让栈继承列表:想复用存储能力,代价是「列表类的所有操作都会出现在栈类的接口上,然而其中大部分操作对一个栈来说并不适用」13。更隐蔽的一种叫「类型与实例名不符实」:车模(car model)和汽车(car)共享属性,但「汽车终归不是模型」——车模的子类实例并不真是超类实例14

两条退路。 「以委托取代子类」把每个子类的差异逻辑搬进委托对象,宿主类持有它——一次解决判据①②:不同变化方向委托给不同对象,委托关系「接口更清晰、耦合更少」15。对熟悉设计模式的读者,作者给了个直译:这就是用状态(State)或策略(Strategy)模式取代子类16。「以委托取代超类」处理判据③:把原来的超类降级成一个字段,宿主类逐个写转发函数——转发「虽然写起来乏味,但它们都非常简单,几乎不可能出错」17。卷轴图书馆的例子:多卷「如何治疗灰鳞病」的卷轴对应目录上的一个条目——卷轴和目录项是两种东西,继承只是图省事的建模错误18

主走查(鸟例的两次变身,数字承自第 09 章): 第一次变身(第 09 章)把 switch 变成了子类:EuropeanSwallow 飞速 35、AfricanSwallow 40 − 2×椰子数、NorwegianBlueParrot 被钉住则 0 否则 10 + 电压/10,各自成为 Bird 的子类。现在引入新需求维度:鸟还要按「进口/本地」区别处理——判据①亮红灯,一张牌不够打了。第二次变身:每个鸟子类的差异逻辑搬进 SpeciesDelegate(物种委托),Bird 持有委托字段;物种继承体系(委托侧)继续保留 35、40−2×椰子数、10+电压/10 各自的实现,而 Bird 自身恢复「未被子类化」的自由身,可以按进口/本地重新子类化。作者总结这次搬家的收益:「新的继承体系范围更收拢了,只涉及各个品种不同的数据和行为」,共用行为留在 Bird 里给未来的子类共享19

7. 「组合优于继承」的准确读法

流行的「对象组合优于类继承」常被读成「继承有害,绝不该用」。作者的两层回应:其一,这条原则「其实是对彼时继承常被滥用的回应」——是特定历史时期的纠偏,不是永恒律法;其二,他的个人策略是「我经常使用继承,部分是因为我知道,如果稍后需要改变,我总可以使用以委托取代子类」——先继承,出问题再换,反正退路一直在20。全书最后给这句口号加了一个诚实的脚注:「“对象组合优于类继承”这句话更确切的表述可能应该是“审慎地组合使用对象组合与类继承,优于单独使用其中任何一种”——不过这就不太上口了」21

8. 作者的判断与证据

判断性质
合理的继承常在演化中浮现,不必预先设计对「按真实世界分类建模」的反驳(经验立场)5
先继承、坏了再委托明确的策略声明,与「组合优于继承」原则一致20
类型与实例名不符实是常见且隐蔽的建模错误作者自造的命名,给了车模/汽车与卷轴两个例子14
「审慎地组合」是那句口号的准确版作者的最终修正,自嘲不上口21

判断(我们的,不是书里的): 三道判据可以合并成一个问题:继承在这里表达的是「分类」还是「复用捷径」? 表达分类(工程师确实是员工)时继承是对的;只想蹭超类的代码(stack 蹭 list、车模蹭汽车)时,委托才是诚实的表达。判据③的两个例子其实都输在这一问上。如果错,会错在: 有些分类本身会漂移(员工转岗、类别可变),此时「真分类」也要退成委托——所以这个问题要随着修改反复重问,答一次不算数。

9. 边界与局限

  • 委托化不是免费的:转发函数成群出现,接口漂移时维护量大增;作者只说它们「几乎不可能出错」,没说它们不烦。
  • 委托体系内部仍是一个继承体系(SpeciesDelegate 有超类)——继承没有消失,只是被圈进了合适的边界。
  • JavaScript 的原型继承与 class 语法差异、接口(interface)在 JS 中不存在等语言细节,书里刻意绕开(它只借 JS 展示手法)。
  • 本章判断句多数是经验立场;原书没有给出「继承 vs 委托」的数字对比数据。

10. 可带走的

  1. 子类之间出现复制粘贴,先统一签名再上移;顺便看差异里有没有漏测的行为。
  2. 超类可以留「陷阱函数」抛错,替未来的子类作者立规矩。
  3. 类型码只在「多函数重复分支」或「类型特有字段」出现时才值得升级成子类。
  4. 类别可变的东西不能用直接继承表达——继承是静态的,字段是动态的。
  5. 子类有阅读成本;用处不够就折叠回字段。
  6. 继承三判据:一个方向的变化、耦合过紧、语义不匹配——中招就换委托。
  7. 「组合优于继承」的正读:先继承,坏了再委托;不是从不继承。
  8. 换委托时,差异逻辑收进委托类,继承体系反而更收拢。

11. 原文地图

主题原书章原文位置
实用却常被误用第12章 开篇text/20-ch12.txt:4(搜「经常被误用」)
重复是温床第12章 §12.1text/20-ch12.txt:38(搜「修改其中一个却未能修改另一个」)
差异暴露漏测第12章 §12.1text/20-ch12.txt:44(搜「忘记测试的行为」)
陷阱函数第12章 §12.1text/20-ch12.txt:116(搜「陷阱(trap」)
继承在演化中浮现第12章 §12.8text/20-ch12.txt:930(搜「演化的过程中才浮现」)
后悔药第12章 §12.8text/20-ch12.txt:935(搜「后悔药」)
类型码与两个诱人之处第12章 §12.6text/20-ch12.txt:401(搜「类型码字段」) · text/20-ch12.txt:406(搜「两个诱人之处」) · text/20-ch12.txt:410(搜「销售目标」)
类别可变不能用直接继承第12章 §12.6text/20-ch12.txt:418(搜「类别是可变的」)
子类有成本第12章 §12.7text/20-ch12.txt:697(搜「子类存在着就有成本」)
一张牌只能打一次第12章 §12.10text/20-ch12.txt:1158(搜「这张牌只能打一次」)
紧密关系第12章 §12.10text/20-ch12.txt:1163(搜「非常紧密的关系」)
stack 继承 list第12章 §12.11text/20-ch12.txt:1970(搜「让栈(stack)继承列表」)
类型与实例名不符实第12章 §12.11text/20-ch12.txt:1982(搜「类型与实例名不符实」)
转发函数乏味但安全第12章 §12.11text/20-ch12.txt:1991(搜「写起来乏味」)
先继承后委托第12章 §12.11text/20-ch12.txt:1995(搜「首先(尽量)使用继承」)
灰鳞病卷轴第12章 §12.11text/20-ch12.txt:2045(搜「灰鳞病」)
委托体系更收拢第12章 §12.10text/20-ch12.txt:1942(搜「范围更收拢」)
审慎地组合第12章 §12.10text/20-ch12.txt:1949(搜「审慎地组合使用」)

Footnotes

  1. 出处:「处理继承关系」第 4 段(text/20-ch12.txt:4,搜「经常被误用」)。

  2. 出处:「处理继承关系」第 37 段(text/20-ch12.txt:37,搜「重复的两个函数」)与第 38 段(text/20-ch12.txt:38,搜「修改其中一个却未能修改另一个」)。

  3. 出处:「处理继承关系」第 44 段(text/20-ch12.txt:44,搜「忘记测试的行为」)。

  4. 出处:「处理继承关系」第 116 段(text/20-ch12.txt:116,搜「陷阱(trap」)。

  5. 出处:「处理继承关系」第 930 段(text/20-ch12.txt:930,搜「演化的过程中才浮现」)。 2

  6. 出处:「处理继承关系」第 935 段(text/20-ch12.txt:935,搜「后悔药」)。

  7. 出处:「处理继承关系」第 401 段(text/20-ch12.txt:401,搜「类型码字段」)与第 403 段(text/20-ch12.txt:403,搜「外部服务」)。

  8. 出处:「处理继承关系」第 406 段(text/20-ch12.txt:406,搜「两个诱人之处」)与第 410 段(text/20-ch12.txt:410,搜「销售目标」)。

  9. 出处:「处理继承关系」第 418 段(text/20-ch12.txt:418,搜「类别是可变的」)。

  10. 出处:「处理继承关系」第 697 段(text/20-ch12.txt:697,搜「子类存在着就有成本」)。

  11. 出处:「处理继承关系」第 1158 段(text/20-ch12.txt:1158,搜「这张牌只能打一次」)。

  12. 出处:「处理继承关系」第 1163 段(text/20-ch12.txt:1163,搜「非常紧密的关系」)。

  13. 出处:「处理继承关系」第 1972 段(text/20-ch12.txt:1972,搜「并不适用」)。

  14. 出处:「处理继承关系」第 1982 段(text/20-ch12.txt:1982,搜「类型与实例名不符实」)。 2

  15. 出处:「处理继承关系」第 1168 段(text/20-ch12.txt:1168,搜「接口更清晰、耦合更少」)。

  16. 出处:「处理继承关系」第 1177 段(text/20-ch12.txt:1177,搜「状态(State)模式或者策略」)。

  17. 出处:「处理继承关系」第 1991 段(text/20-ch12.txt:1991,搜「写起来乏味」)。

  18. 出处:「处理继承关系」第 2045 段(text/20-ch12.txt:2045,搜「灰鳞病」)。

  19. 出处:「处理继承关系」第 1942 段(text/20-ch12.txt:1942,搜「范围更收拢」)。

  20. 出处:「处理继承关系」第 1172 段(text/20-ch12.txt:1172,搜「我经常使用继承」)与第 1995 段(text/20-ch12.txt:1995,搜「首先(尽量)使用继承」)。 2

  21. 出处:「处理继承关系」第 1949 段(text/20-ch12.txt:1949,搜「审慎地组合使用」)。 2