处理继承关系
改进设计的一个重要方向就是消除重复代码
在一些弱类型语言,比如lua处理继承更要小心,由于覆盖函数很轻易,当继承自某个类时要时刻注意该函数实现是否父类已有定义,父类的实现是否需要调用。基于这点不太建议继承层次太深
12.1 & 12.2 函数上移(Pull Up Method)、字段上移(Pull Up Field)
核心原则是避免代码或字段的重复(将重复部分挪到父类),重复代码假以时日只会成为滋生 bug 的温床
注意点:
- 当上移函数实现中调用了父类不需要实现的函数,建议留个能明确传达出“继承的子类需要提供该函数实现”这个信息的陷阱(trap)函数
1 | protected virtual void XxxFunc() { |
12.3 构造函数本体上移(Pull Up Constructor Body)
与普通的函数上移有以下注意点:
- 注意调用顺序,先执行基类构造
- 当多个子类构造调用了同一段代码,且代码里依赖子类构造的成员时,此时就存在时序问题不能上移。但可以采用普通函数上移的方式去处理(子类构造调用)
12.4 & 12.5 函数下移(Push Down Method)、字段下移(Push Down Field)
- 将只是某个子类关心的函数或字段迁移回真正关心它的子类
- 如果不清楚有哪些子类关心,可以【多态取代条件表达式】只保留公有部分
当有相同的行为却又依赖不同类型的子类数据时,多态是很好的助手,比如技能编辑器检测类,只保留通用的检测接口在父类,并不需要关心具体的数据,子类实现时才需要维护对应数据
12.6 & 12.7 以子类取代类型码(Replace Type Code with Subclasses)、移除子类(Remove Subclass)
1 | function createEmployee(name, type) { |
处理什么问题:
- “相似但又不同的东西”常用类型码去区分不同的部分
- 比如我手中的芒果,在认知里芒果就是芒果,但却又有很多细分品种,有种不同的甜度和颜色
- 当不同的部分需要额外去处理,类中难免会出现if分支语句(视情况是芒果香味还是腐烂味)
使用子类取代类型码的好处:
- 减少类方法臃肿,使用多态来将分支语句拆分给子类
- 减少类变量臃肿,将不同的部分需要关心的成员拆分给子类
不使用子类取代的理由:
- 类型码并不会影响具体的行为时,此时拆分子类反而有理解成本
- 比如给芒果区分性别,本身也没有太多意义
是否需要取代,我觉得要看需要处理不同的位置数量是否多,以及该类后续是否需要频繁维护新增不同的部分。
一些注意的点:
是直接处理携带类型码的这个类,还是应该处理类型码本身呢
- 看该类是否可以由多个类型码组合
- 该类是否可以随意变更类型码
12.8 & 12.9 提炼超类(Extract Superclass)、折叠继承体系(Collapse Hierarchy)
- 将两个类的相似之处提取到超类(合理的继承关系是在程序演化的过程中才浮现出来的:发现共同元素,抽取到一处,产生继承关系)
- 去除提取后与超类行为一致的子类来折叠继承关系,减少理解成本
12.10 以委托取代子类(Replace Subclass with Delegate)
1 | class Order { |
使用继承的一些短板:
- 继承只能用于处理一个方向上的变化
- 比如说游戏内物体可以按怪物、宠物、角色去分类,但如果后续引入了新分类,比如飞行生物、陆地生物,如果只用继承很难去处理这种多种方向的变化(部分语言支持多重继承)
- 继承给类之间引入了非常紧密的关系,在超类上做任何修改,都很可能破坏子类
一些案例:
- 游戏中常用的组件模式,将单位所拥有的能力拆分成一个个委托类,需要哪些能力可以自由组合,委托类也不需要过度去关心单位类的结构。包括上面短板中提到会按不同类型去分类,类型本身也可以拆成委托类,负责获取不同的数据和一些有差异的行为。
- 编辑器等界面交互UI,只负责分区的组织和上层框架的绘制,具体的细节可以交给委托类去绘制。
对象组合优于类继承?
- 正确的解读是:审慎地组合使用对象组合与类继承,优于单独使用其中任何一种
12.11 以委托取代超类(Replace Superclass with Delegate)
1 | class List {...} |
- 避免为了让某个类使用另一个类的功能,从而强行继承
- 比如上面的例子中,列表的接口会出现在栈中,并且栈本身也不是一个列表
- 合理的做法是去持有另一个类作为委托类,将需要用到的功能通过转发函数转发
小结
再读重构还是有不少收获,回想第一次读时更多关心代码功能,并没有多少后续维护的概念,囫囵吞枣般没有太多共鸣。
为什么要重构?
重构本身不会改变程序运行逻辑,还有不少成本引入。
书中给了答案:减少重复代码,增加代码可读性,降低后续修改难度
工作一段时间最大的感悟便是需求是时刻在变化的,程序设计也要跟着不断变化。好的代码设计能够很好面对变化,引入新的功能风险、成本都能够在可控范围。特别是一些很紧急的需求,如果原先底层不支持,就只能各种特判代码安排了。当然有些情况是不可避免的,关键是后续使用“重构”去让那些临时代码不再临时,即使是屎山也要不断铲屎,避免越堆越高。
一些读完日常代码中改进的写法
- 循环中避免做过多事情,以前总觉得一遍循环能处理这么多事很酷,还省性能。实际还是要看运行情况,如果本身就不会频繁调用,省下的性能导致增加的维护成本,实在得不偿失,该拆循环就得拆。
- 避免在函数传入参数来决定不同的分支,特别是bool参数,会让调用方比较混乱,最好还是拆分多个函数。
- 慎重对待可变数据,bug滋生的源头。