前两篇文章 《识别代码中的坏味道(一)》 和 《识别代码中的坏味道(二)》 中已经介绍了 18 个代码坏味道。《重构》中还涉及到另外 4 个代码坏味道,本文将将详细介绍剩余的 4 个代码坏味道。
这四个代码坏味道是:
- 中间人(Middle Man)
- 狎昵关系
- 不完美的库类
- 被拒绝的遗赠
01 中间人(Middle Man)
在上一篇文章中 《识别代码中的坏味道(二)》 中在“过度耦合的消息链”这种代码坏味道曾经提及过中间人(Middle Man)这种代码坏味道,那么中间人到底是一类什么代码呢?
中间人指的是一种过度使用委托的代码,《重构》中给了一个参考值,如果一个类中有一半的方法都委托给其他对象进行,
为什么中间人是一种代码坏味道?
过度使用委托。这意味着当需求发生某些的变化的时候,这个中间人的类总是被牵连进来一并修改。这种中间人代码越多,浪费掉的时间也就越多。
如何解决中间人这种代码坏味道?
中间人的代码在于过度使用和委托两点。因此解决中间人这种代码坏味道就应该从减少委托下手:
删除中间人的方法,可以使用 Remove Middle Man(移除中间人)这种重构技巧。
当然如果原有代码的代理类中并不怎么变化,也可以选择延迟重构,依照“事不过三,三则重构”的原则可以选择当发生变化的时候进行重构。
02 狎昵关系(Inappropriate Intimacy)
指的是类之间花费太多的时间去探究彼此的私有的属性或者方法。
造成狎昵关系的原因可能是:
两个类本来就不应该拆分开;
两个类之间存在双向关联;
-
因为继承导致了狎昵关系;
...
为什么狎昵关系是一种代码坏味道?
- 狎昵关系会导致强耦合的表现;
- 而且类和类之间的职责将会变得模糊;
- 会因为访问对方的私有信息而导致过多的操作出现,或者产生封装上的妥协,让两个类纠缠不清。
如何解决狎妮关系这种代码坏味道?
- 通过 Move Field (搬移属性),Move Method(搬移方法)来移动属性和方法的位置,让属性和方法移动到它们本应该出现的位置。
- 如果直接移动属性和方法并不合适,可以尝试使用 Extract Class(提炼类)看是否能够找到公共类。
- 如果是因为相互调用导致的问题,可以尝试 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)尝试将关联关系划清。
- 如果是因为继承导致狎昵关系,可以尝试移除继承关系,改用代理类来实现。
03 不完美的库类
当直接使用第三方库的时候,导致代码可读性变差、意图不明确的问题。
为什么不完美的库类是一种代码坏味道?
第三方类库提供的功能能够在很场景下被复用。但是放在业务场景下,却总是要从业务视角切换到单纯的技术视角来来使用某些第三方类库。
例如
Date newStart = new Date(
previousEnd.getYear(),
previousEnd.getMonth(),
previousEnd.getDate() + 1);
一眼看上去这是在表达什么意思其实并不容易看到。不完美的类库就在于造成代码中语意化变差。
如何解决不完美库类这种代码坏味道?
很多开发者会采用注释的方式期望让代码可读,但是这类注释本身也是一种代码的坏味道。不过可以借助函数名来揭示意图。
所以遇到上面例子的情况,可以使用 Extract Method 来提炼一个函数,生成如下代码
Date newStart = nextDay(previousEnd);
...
private Date nextDay(Date previousEnd) {
return new Date(
previousEnd.getYear(),
previousEnd.getMonth(),
previousEnd.getDate() + 1);
}
这样调用 nextDay() 的地方,就可以轻松的知道获取到 previousEnd 日期的下一天日期。
如果一个类中存在多种这种调用,或者多个类中都有类似的函数的时候,提炼一个单独一个类,并通过这个类对外提供这些方法无疑是一种消除重复提高复用的办法。实现这个类的方式可以使用代理的方式,也可以使用继承的方式。如果一个类只是提供代理方法,具体实现都要委托给类库,这样情况下,不如使用继承来生成的子类,并在子类中添加那些可以复用的方法。重构的过程可以参考 Introduce Local Extension(引入本地扩展)。
很显然第三方类库被设计的出发点往往是好的,但是实际调用的时候除了享受这种快速实现的方式,还需要关注第三方类库给当前项目带来的一些坏味道,并着手解决这些问题。
04 拒绝的遗赠
这个坏味道指的是当子类继承基类的时候,父类的一些方法即使子类并不需要也被迫被继承的情况。出现这种坏味道的一般有两种原因:
- 继承体系设计的不好,还需要调整;
- 基类实现了某个接口,导致子类不需要的时候也会实现那个接口对应的方法。
详细的例子可以参考:《重构分析21: 被拒绝的遗赠(Refused Bequest)》
为什么拒绝的遗赠是一种代码坏味道?
这个坏味道主要原因就是继承带来的坏味道,子类被迫实现某些方法或者从父类继承的方法对自身不但没有帮助甚至造成误导,比如:代码中通过继承实现 正方形 继承 长方形并求面积的例子,感兴趣可以参考《敏捷软件开发原则、模式、实践》中的里氏替换原则。
如何解决拒绝的遗赠的这种代码坏味道?
有两种思路:
改善继承体系。剔除子类不需要的方法,并创建子类的兄弟,通过 Move Method 将不需要的方法移动到兄弟类中,通过 Move Field 将涉及到非公共属性也移动到兄弟子类中。
使用代理来取代继承。这种方式的修改只涉及到对子类的调整,影响范围较小,并且也不会因此而像第一种重构方法那样因为要维护继承体系而导致一些新概念的产生。同时还能避免因为基类继承了某个接口,而导致的子类被迫实现某些方法的情况。
总结
至此 22 种常见的代码坏味道已经介绍完成。关注工具、框架的同时花一部分精力关注代码质量,能够让项目随着时间不断演进。当然实际工作中遇到的坏味道往往比这 22 种还要多。
项目中我们可以使用 Check Style、PMD、Arch Unit 帮助我们及时发现项目中的问题,但是更”软“的部分需要我们花精力来理解清楚是什么、为什么、怎么解决。
或许你也已经发现了,很多情况下坏味道的原因在于变化时,无法快速应对变化,有的是代码设计的问题,有的是可读性的问题。即使代码坏味道也分为强烈的坏味道和淡淡的坏味道,所以重构的原则也是”事不过三、三则重构“,因此面对代码坏味道的时候如果代码坏味道很淡我们可以延迟消除坏味道。如果坏味道已经很强烈,或者淡淡的坏味道因为频繁的变化而导致效率下降时,那就不如先解决这种坏味道。
扩展
重构不是发生在项目结束的时候,而是融入在天天工作中进行的。采用TDD(测试驱动开发的方式)是一种很好的选择,熟悉TDD以及测试工具的情况下,TDD 不但不会降低速度反而让思路更加严谨、实现的代码实现质量更高。感兴趣的话可以参考:
参考
《重构》