11-12¶
11. 重构 API¶
11.1 将查询函数和修改函数分离(Separate Query from Modifier)¶
- 没有副作用的函数
如果某个函数只是提供一个值,没有任何看得到的副作用,那么这是一个很有价值的东西。我可以任意调用这个函数,也可以把调用动作搬到调用函数的其他地方。
- 命令与查询分离
任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离(Command-Query Separation)。
具体重构步骤
- 复制整个函数,将其作为一个查询来命名;
如果想不出好名字,可以看看函数返回的是什么。查询的结果会被填入一个变量,这个变量的名字应该能对函数如何命名有所启发。
- 从新建的查询函数中去掉所有造成副作用的语句;
- 执行静态检查;
- 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试;
- 从原函数中去掉返回值;
- 测试;
完成重构之后,查询函数与原函数之间常会有重复代码,可以做必要的清理。
11.2 函数参数化(Parameterize Function)¶
- 合并函数消除重复
若发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。
这个重构可以使函数更有用,因为重构后的函数还可以用于处理其他的值。
具体重构步骤
- 从一组相似的函数中选择一个;
- 运用改变函数声明。把需要作为参数传入的字面量添加到参数列表中;
- 修改该函数所有的调用处,使其在调用时传入该字面量值;
- 测试;
- 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试;
- 对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试;
如果第一个函数经过参数化以后不能直接替代另一个与之相似的函数,先对参数化之后的函数做必要的调整,再做替换。
11.3 移除标记参数(Remove Flag Argument)¶
- 标记参数
“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。
- 隐藏差异性
标记参数让人难以理解到底有哪些函数可以调用、应该怎么调用。拿到一份API以后,首先看到的是一系列可供调用的函数,但标记参数却隐藏了函数调用中存在的差异性。
- 直接传入字面值
并非所有类似这样的参数都是标记参数。如果调用者传入的是程序中流动的数据,这样的参数不算标记参数;只有调用者直接传入字面量值,这才是标记参数。
- 使代码更整洁
移除标记参数不仅使代码更整洁,并且能帮助开发工具更好地发挥作用。去掉标记参数后,代码分析工具能更容易地体现出“高级”和“普通”两种预订逻辑在使用时的区别。
具体重构步骤
- 针对参数的每一种可能值,新建一个明确函数;
如果主函数有清晰的条件分发逻辑,可以用分解条件表达式(260)创建 明确函数;否则,可以在原函数之上创建包装函数。
- 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数;
11.4 保持对象完整(Preserve Whole Object)¶
- 传递整个记录
“传递整个记录”的方式能更好地应对变化:如果将来被调的函数需要从记录中导出更多的数据,就不用为此修改参数列表。并且传递整个记录也能缩短参数列表,让函数调用更容易看懂。
- 依恋情结
从一个对象中抽取出几个值,单独对这几个值做某些逻辑操作,这是一种代码坏味道(依恋情结),通常标志着这段逻辑应该被搬移到对象中。
- 提炼类的时机
如果几处代码都在使用对象的一部分功能,可能意味着应该用提炼类把这一部分功能单独提炼出来。
具体重构步骤
- 新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数);
给这个函数起一个容易搜索的名字,这样到重构结束时方便替换。
- 在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据);
- 执行静态检查;
- 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试;
修改之后,调用处用于“从完整对象中导出参数值”的代码可能就没用了,可以用移除死代码去掉。
- 所有调用处都修改过来之后,使用内联函数把旧函数内联到新函数体内;
- 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处;
11.5 以查询取代参数(Replace Parameter with Query)¶
- 总结函数可变性的参数列表
函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解。
- 不必要的参数会增加调用者的难度
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。这个本不必要的参数会增加调用者的难度,因为它不得不找出正确的参数值,其实原本调用者是不需要费这个力气的。
- 获得正确参数值的责任
去除参数也就意味着“获得正确的参数值”的责任被转移:有参数传入时,调用者需要负责获得正确的参数值;参数去除后,责任就被转移给了函数本身。
- 移除参数带来的依赖关系
不使用以查询取代参数最常见的原因是,移除参数可能会给函数体增加不必要的依赖关系——迫使函数访问某个程序元素,而我原本不想让函数了解这个元素的存在。
- 查询取代参数的安全场景
如果想要去除的参数值只需要向另一个参数查询就能得到,这是使用以查询取代参数最安全的场景。如果可以从一个参数推导出另一个参数,那么几乎没有任何理由要同时传递这两个参数。
- 处理函数具有引用透明性
如果在处理的函数具有引用透明性(referential transparency,即不论任何时候,只要传入相同的参数值,该函数的行为永远一致),这样的函数既容易理解又容易测试。通常不会去掉它的参数,让它去访问一个可变的全局变量。
具体重构步骤
- 如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中;
- 将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试;
- 全部替换完成后,使用改变函数声明将该参数去掉;
11.6 以参数取代查询(Replace Query with Parameter)¶
- 权衡改变代码依赖关系
需要使用本重构的情况大多源于我想要改变代码的依赖关系——为了让目标函数不再依赖于某个元素,即把这个元素的值以参数形式传递给该函数。
这里需要注意权衡:如果把所有依赖关系都变成参数,会导致参数列表冗长重复;如果作用域之间的共享太多,又会导致函数间依赖过度。
- 引用透明性带来的益处
如果一个函数使用了另一个元素,而后者不具引用透明性,那么包含该元素的函数也就失去了引用透明性。只要把“不具引用透明性的元素”变成参数传入,函数就能重获引用透明性。
虽然这样就把责任转移给了函数的调用者,但是具有引用透明性的模块能带来很多益处。
- 常见的设计模式
在负责逻辑处理的模块中只有纯函数,其外再包裹处理 I/O 和其他可变元素的逻辑代码。借助以参数取代查询,可提纯程序的某些组成部分,使其更容易测试、更容易理解。
- 增加函数调用者的复杂度
把查询变成参数以后,就迫使调用者必须弄清如何提供正确的参数值,这会增加函数调用者的复杂度,而在设计接口时通常更愿意让接口的消费者更容易使用。
具体重构步骤
- 对执行查询操作的代码使用提炼变量,将其从函数体中分离出来;
- 现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数;
给提炼出的新函数起一个容易搜索的名字,以便稍后改名。
- 使用内联变量,消除刚才提炼出来的变量;
- 对原来的函数使用内联函数;
- 对新函数改名,改回原来函数的名字;
11.7 移除设置函数(Remove Settings Method)¶
- 排除被修改的可能性
该字段就只能在构造函数中赋值,我“不想让它被修改”的意图会更加清晰,并且可以排除其值被修改的可能性——这种可能性往往是非常大的。
- 清晰地表达意图
去除设值函数,清晰地表达“构造之后不应该再更新字段值”的意图。
具体重构步骤
- 如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值;
如果想移除多个设值函数,可以一次性把它们的值都传入构造函数,这能简化后续步骤。
- 移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试;
如果不能把“调用设值函数”替换为“创建一个新对象”(例如需要更新一个多处共享引用的对象),请放弃本重构。
- 使用内联函数消去设值函数。如果可能的话,把字段声明为不可变;
- 测试;
11.8 以工厂函数取代构造函数(Replace Constructor with Factory Function)¶
- 构造函数的局限性
但与一般的函数相比,构造函数又常 有一些丑陋的局限性。例如,Java的构造函数只能返回当前所调用类的实例,即无法根据环境或参数信息返回子类实例或代理对象;构造函数的名字是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的操作符来调用(在很多语言中是new关键字),所以在要求普通函数的场合就难以使用。
- 工厂函数不受限制
工厂函数就不受这些限制。工厂函数的实现内部可以调用构造函数,但也可以换成别的方式实现。
具体重构步骤
- 新建一个工厂函数,让它调用现有的构造函数;
- 将调用构造函数的代码改为调用工厂函数;
- 每修改一处,就执行测试;
- 尽量缩小构造函数的可见范围;
11.9 以命令取代函数(Replace Function with Command)¶
- 命令对象
函数,不管是独立函数,还是以方法形式附着在对象上的函数, 是程序设计的基本构造块。
不过,将函数封装成自己的对象有时也是一种有用的办法。这样的对象称之为“命令对象”(command object),或简称“命令”(command)。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,就是这种对象存在的意义。
- 命令对象带来的好处
与普通的函数相比,命令对象提供了更大的控制灵活性和更强的表达能力。除了函数调用本身,命令对象还可以支持附加的操作,例如撤销操作。
可通过命令对象提供的方法来设值命令的参数值,从而支持更丰富的生命周期管理能力。
可借助继承和钩子对函数行为加以定制。如果我所使用的编程语言支持对象但不支持函数作为一等公民,通过命令对象就可以给函数提供大部分相当于一等公民的能力。
同样,即便编程语言本身并不支持嵌套函数,也可借助命令对象的方法和字段把复杂的函数拆解开,而且在测试和调试过程中可以直接调用这些方法。
- 命令对象带来的复杂性取舍
如果要在作为一等公民的函数和命令对象之间做个选择,通常都会选函数。只有特别需要命令对象提供的某种能力而普通的函数无法提供这种能力时,才会考虑使用命令对象。
具体重构步骤
- 为想要包装的函数创建一个空的类,根据该函数的名字为其命名;
- 使用搬移函数把函数移到空的类里;
保持原来的函数作为转发函数,至少保留到重构结束之前才删除。
- 遵循编程语言的命名规范来给命令对象起名。如果没有合适的命名规范,就给命令对象中负责实际执行命令的函数起一个通用的名字;
- 可考虑给每个参数创建一个字段,并在构造函数中添加对应的参数;
11.10 以函数取代命令(Replace Command with Function)¶
- 命令对象提供的强大机制
命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松地将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。
- 不复杂使用的场景
大多数时候只是想调用一个函数,让它完成自己的工作就好。若这个函数不是太复杂,那么命令对象可能显得费而不惠,那么就应该考虑将其变回普通的函数。
具体重构步骤
- 运用提炼函数,把“创建并执行命令对象”的代码单独提炼到一个函数中;
这一步会新建一个函数,最终这个函数会取代现在的命令对象。
- 对命令对象在执行阶段用到的函数,逐一使用内联函数;
如果被调用的函数有返回值,请先对调用处使用提炼变量,然后再使用内联函数。
- 使用改变函数声明,把构造函数的参数转移到执行函数;
- 对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试;
- 把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数);
- 测试;
- 用移除死代码把命令类消去;
12. 处理继承关系¶
12.1 函数上移(Pull Up Method)¶
- 避免重复代码
避免重复代码是很重要的。重复的两个函数现在也许能够正常工作,但假以时日却只会成为滋生bug的温床。
无论何时,只要系统内出现重复,你就会面临“修改其中一个却未能修改另一个”的风险。通常找出重复也有一定的难度。
- 函数上移的适用场合
如果某个函数在各个子类中的函数体都相同,这就是最显而易见的函数上移适用场合。当然,情况并不总是如此明显。
- 引用只出现子类而不出现超类的特性
函数上移过程中最麻烦的一点就是,被提升的函数可能会引用只出现于子类而不出现于超类的特性。此时就得用字段上移和函数上移先将这些特性(类或者函数)提升到超类。
- 借助塑造模版函数
如果两个函数工作流程大体相似,但实现细节略有差异,那么会考虑先借助塑造模板函数(Form Template Method)构造出相同的函数,然后再提升它们。
具体重构步骤
- 检查待提升函数,确定它们是完全一致的;
如果它们做了相同的事情,但函数体并不完全一致,那就先对它们进行重构,直到其函数体完全一致。
- 检查函数体内引用的所有函数调用和字段都能从超类中调用到;
- 如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名;
- 在超类中新建一个函数,将某一个待提升函数的代码复制到其中;
- 执行静态检查;
- 移除一个待提升的子类函数;
- 测试;
- 逐一移除待提升的子类函数,直到只剩下超类中的函数为止;
12.2 字段上移(Pull Up Field)¶
- 重复字段
如果各子类是分别开发的,或者是在重构过程中组合起来的,你常会发现它们拥有重复特性,特别是字段更容易重复。这样的字段有时拥有近似的名字,但也并非绝对如此。
判断若干字段是否重复,唯一的办法就是观察函数如何使用它们。如果它们被使用的方式很相似,就可将它们提升到超类中去。
- 减少重复
本项重构可从两方面减少重复:首先它去除了重复的数据声明;其次它使可将使用该字段的行为从子类移至超类,从而去除重复的行为。
具体重构步骤
- 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用;
- 如果这些字段的名称不同,先使用变量改名为它们取个相同的名字;
- 在超类中新建一个字段;
新字段需要对所有子类可见(在大多数语言中 protected 权限便已足够)。
- 移除子类中的字段;
- 测试;
12.3 构造函数本体上移(Pull Up Constructor Body)¶
- 构造函数的特殊
它们不是普通函数,使用它们比使用普通函数受到更多的限制。附加了特殊的规则,对一些做法与函数的调用次序有所限制。
具体重构步骤
- 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数;
- 使用移动语句将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后;
- 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数;
- 测试;
- 如果存在无法简单提升至超类的公共代码,先应用提炼函数,再利用函数上移提升之;
12.4 函数下移(Push Down Method)¶
- 超类真正关心的子类
如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。
- 函数下移的适用场景
这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,那就得用以多态取代条件表达式,只留些共用的行为在超类。
具体重构步骤
- 将超类中的函数本体复制到每一个需要此函数的子类中;
- 删除超类中的函数;
- 测试;
- 将该函数从所有不需要它的那些子类中删除;
- 测试;
12.5 字段下移(Push Down Field)¶
如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。
具体重构步骤
- 在所有需要该字段的子类中声明该字段;
- 将该字段从超类中移除;
- 测试;
- 将该字段从所有不需要它的那些子类中删掉;
- 测试;
12.6 以子类取代类型码(Replace Type Code with Subclasses)¶
- 继承的诱人之处
继承有两个诱人之处:首先可用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,就可用以多态取代条件表达式来处理这些函数。
- 对特定类型码取值的意义
有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时可创建子类,然后用字段下移把这样的字段放到合适的子类中去。
当然也可加入验证逻辑,确保只有当类型码取值正确时才使用该字段,不过子类的形式能更明确地表达数据与类型之间的关系。
- 子类取代类型码需要考虑的
在使用以子类取代类型码时,需要考虑一个问题:应该直接处理携带类型码的这个类,还是应该处理类型码本身呢?
具体重构步骤
- 自封装类型码字段;
- 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值;
- 创建一个选择器逻辑,把类型码参数映射到新的子类;
如果选择直接继承的方案,就用以工厂函数取代构造函数包装构造函数,把选择器逻辑放在工厂函数里;
如果选择间接继承的方案,选择器逻辑可以保留在构造函数里。
- 测试;
- 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试;
- 去除类型码字段;
- 测试;
- 使用函数下移和以多态取代条件表达式处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数;
12.7 移除子类(Remove Subclass)¶
- 子类失去价值
但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。
有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
- 子类存在的成本
子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。
具体重构步骤
- 使用以工厂函数取代构造函数,把子类的构造函数包装到超类的工厂函数中;
如果构造函数的客户端用一个数组字段来决定实例化哪个子类,可以把这个判断逻辑放到超类的工厂函数中。
- 如果有任何代码检查子类的类型,先用提炼函数把类型检查逻辑包装起来,然后用搬移函数将其搬到超类。每次修改后执行测试;
- 新建一个字段,用于代表子类的类型;
- 将原本针对子类的类型做判断的函数改为使用新建的类型字段;
- 删除子类;
- 测试;
本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。
12.8 提炼超类(Extract Superclass)¶
- 继承关系的出现
真实世界的分类结构可以作为设计继承关系的提示,但还有很多时候,合理的继承关系是在程序演化的过程中才浮现出来的:发现了一些共同元素,希望把它们抽取到一处,于是就有了继承关系。
- 继承和委托的选择
另一种选择就是提炼类。这两种方案之间的选择,其实就是继承和委托之间的选择,总之目的都是把重复的行为收拢一处。
提炼超类通常是比较简单的做法,所以会首选这个方案。即便选错了,也总有以委托取代超类这瓶后悔药可吃。
具体重构步骤
- 为原本的类新建一个空白的超类;
如果需要的话,用改变函数声明调整构造函数的签名。
- 测试;
- 使用构造函数本体上移、函数上移和字段上移手法,逐一将子类的共同元素上移到超类;
- 检查留在子类中的函数,看它们是否还有共同的成分。如果有可以先用提炼函数将其提炼出来,再用函数上移搬到超类;
- 检查所有使用原本的类的客户端代码,考虑将其调整为使用超类的接口;
12.9 折叠继承体系(Collapse Hierarchy)¶
随着继承体系的演化,有时会发现一个类与其超类已经没多大差别,不值得再作为独立的类存在。此时可选择把超类和子类合并起来。
具体重构步骤
- 选择想移除的类:是超类还是子类?
选择的依据是看哪个类的名字放在未来更有意义。如果两个名字都不够好,我就随便挑一个。
- 使用字段上移、字段下移、函数上移和函数下移,把所有元素都移到同一个类中;
- 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类;
- 移除我们的目标;此时它应该已经成为一个空类;
- 测试。
12.10 以委托取代子类(Replace Subclass with Delegate)¶
- 继承的短板
但继承也有其短板。最明显的是,继承这张牌只能打一次。导致行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。
更大的问题在于,继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类,所以必须非常小心,并且充分理解子类如何从超类派生。如果两个类的逻辑分处不同的模块、由不同的团队负责,问题就会更麻烦。
- 委托解决继承的问题
对于不同的变化原因,可委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。因此,继承关系遇到问题时运用以委托取代子类是常见的情况。
- 组合优于继承的原则
这条原则之所以强调“组合优于继承”,其实是对彼时继承常被滥用的回应。
- 状态模式或策略模式
就是用状态(State)模式或者策略(Strategy)模式取代子类。
这两个模式在结构上是相同的,都是由宿主对象把责任委托给另一个继承体系。 以委托取代子类并非总会需 要建立一个继承体系来接受委托,不过建立一个状态或策略的继承体系经常都是有用的。
具体重构步骤
- 如果构造函数有多个调用者,首先用以工厂函数取代构造函数把构造函数包装起来;
- 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用;
- 在超类中添加一个字段,用于安放委托对象;
- 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例;
这一步可以在工厂函数中完成,也可以在构造函数中完成(如果构造函数有足够的信息以创建正确的委托对象的话)。
- 选择一个子类中的函数,将其移入委托类;
- 使用搬移函数手法搬移上述函数,不要删除源类中的委托代码;
如果这个方法用到的其他元素也应该被移入委托对象,就把它们一并搬移。如果它用到的元素应该留在超类中,就在委托对象中添加一个字段,令其指向超类的实例。
- 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码去掉已经没人使用的委托代码;
如果有多个委托类,并且其中的代码出现了重复,就使用提炼超类手法消除重复。此时如果默认行为已经被移入了委托类的超类,源超类的委托函数就不再需要卫语句了。
- 测试;
- 重复上述过程,直到子类中所有函数都搬到委托类;
- 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数;
- 测试;
- 运用移除死代码去掉子类;
12.11 以委托取代超类(Replace Superclass with Delegate)¶
- 继承造成的困扰
若只要继承一个已有的类,覆写一些功能,再添加一些功能,就能达成目的。但继承也有可能造成困扰和混乱。
- 合理的继承关系特征
除了“子类用得上超类的所有函数”之外,合理的继承关系还有一个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应该完全不出问题。
- 使用委托解决继承的问题
在这两个例子中,有问题的继承招致了混乱和错误——如果把继承关系改为将部分职能委托给另一个对象,这些混乱和错误本是可以轻松避免的。
使用委托关系能更清晰地表达“这是另一个东西,我只是需要用到其中携带的一些功能”这层意思。
- 使用委托取代超类
即便在子类继承是合理的建模方式的情况下,如果子类与超类之间的耦合过强,超类的变化很容易破坏子类的功能,还是选择使用以委托取代超类。
这样做的缺点就是,对于宿主类(也就是原来的子类)和委托类(也就是原来的超类)中原本一样的函数,现在我必须在宿主类中挨个编写转发函数。不过还好,这种转发函数虽然写起来乏味,但它们都非常简单,几乎不可能出错。
- 继承和委托的选择
如果符合继承关系的语义条件(超类的所有方法都适用于子类,子类的所有实例都是超类的实例),那么继承是一种简洁又高效的复用机制。
如果情况发生变化,继承不再是最好的选择,也可比较容易地运用以委托取代超类。所以建议是首先(尽量)使用继承,如果发现继承有问题,再使用以委托取代超类。
具体重构步骤
- 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例;
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试;
大多数时候,每转发一个函数就可以测试,但一对设值/取值必须同时转移,然后才能测试。
- 当所有超类函数都被转发函数覆写后,就可以去掉继承关系;