《重构:改善既有代码的设计》 —— 重构概念及代码坏味道

前言

本文主要来自《重构—改善既有代码的设计》的一些读书笔记和摘录,在对应的地方都标注上了书中的对应页数,可以结合原书服用。刚开始看这些所谓的重构方法可能是一头雾水,看完一遍书以后对应一些方法可能同样也是一头雾水。

这里简单谈一下读完这本书我个人的收获以及感受吧。首先这可以理解为一本手册,介绍各种重构方法 61 个,我们不可能一次就把所有的方法期望可以记住,并且吸收。但是阅读完本书,对书中作者表达的一些观点很有收获,例如对测试构建的意义,包括在介绍不同重构方法时,字里行间表达数据和函数之间关系的理解,模块化设计的描述等,整篇读下来逐渐让自己有意识的看自己以前写的代码,包括之后要写的代码都多少能朝着避开 “bad smell” 的方向努力。

本系列文章暂时不考虑对重构方法补充对应的代码示例,一是 61 个方法很多情况,很多细节,很多自己了解的也不是那么清晰,很难表达到尾,逐渐陷入细节就不好了。二是本文更多地是想摘录一些我本人觉得写的比较好的观点和表达,更多详细的内容还是要更建议去阅读原书。

下面设计到重构方法的内容,括号中的数字表示对应原书的页数。

对于重构方法目录,放在了另一篇博客中:

《重构:改善既有代码的设计》 —— 重构方法目录

基础概念

这里主要围绕以下几个问题来的。

重构是什么?

名词定义:对软件内部的一种调整,目的是不改变软件可观察行为的前提下,提高其可理解性,降低其成本。

动词定义:使用一系列重构手法,在不改变软件任何可观察行为的前提下,调整其结构。

重构是微步的,且是保持软件行为的情况下做修改,一步步达成大规模的修改,若有人重构说自己代码一两天内不能使用,那他肯定不是重构。

为什么应该重构?
  • 改进软件设计(消除重复代码)
  • 使软件更易理解
  • 帮助找到 bug
  • 提高编程速度
该在什么地方重构?
  • 首先如果需要给程序添加一个特性,却发现代码缺乏良好地结构而不易更改,则应该先重构这个程序,使其比较容易添加特性,再开始特性的添加。
  • 重构前要先检查自己是否有一套可靠的测试集
  • 回头看代码能告诉我它在干什么,不需要重新思考一下
  • 重构时小步修改,然后修改完就运行测试
  • 大多数情况下可以先忽略重构带来的性能损耗,先重构,有需要再做性能优化
  • 好代码的检验标准是人们是否能轻易地修改它
重构名录,需要时详细阅读。

本书提供了详细的重构手法,当然很难说每一个都记住,但可以作为重构手册,在有需要的时候翻阅,查找对自己有帮助的方法。

并且本书在第一章用了一个很详细的 demo 示例来作为概念介绍,展示作者如何以及为什么用一些重构手法来对程序做改动,引出重构的直观效果。感兴趣的读者建议阅读原书。

代码中的坏味道

下面是书中对代码坏味道的一个罗列,以及针对不同情况可能可以使用的一些重构手法,它们相关之间不可避免的是有重叠的,并且重构方法之间很多时候也是一个配合关系,例如没有可以搬移的函数(198),可以先移动语句(223),再来提炼函数(106),然后搬运。重点还是了解代码中的坏味道有哪些,对应坏味道书中提到的一些重构方法只是简单记录,不用拘泥于有哪些方法这样的细节,等到真正需要用到的时候权当一个索引。

  1. 神秘命名:给模块、函数、变量、类的命名应该使它们能清晰表达自己的功能和用法,而不是随手给一个令人困惑或者毫无意义的名字。可以参考的重构方法有:改变函数声明(124),变量改名(137),字符安改名(244)等。

  2. 重复代码:重复带来的危害不言而喻,在修改代码是必须小心对比,找到所有应该修改的地方。根据是否只是最单纯的重复,还是代码相似但是不完全相同,以及发生在类中,可以参考的重构方法有:提炼函数(106),移动语句(223),函数上移(350)等。

  3. 过长函数:函数越长越难理解,分解函数的一个重要原则是,每当感觉需要以注释来说明点什么的时候,我们就需要把说明的东西写进一个独立函数中,并以用途命名。根据函数的不同情况,是否有大量的参数和临时变量,可以参考的重构方法有:提炼函数(106),查询取代临时变量(178),引入参数对象(140),保持对象完整(319),以命令取代函数(337)等。如果函数中涉及到条件表达式和循环,可以使用分解条件表达式(260),多态取代条件表达式(272),拆分循环(227)等方法。

  4. 过长参数列表:过长的参数列表本身就会令人迷惑,可以使用的重构方法有:查询取代参数(324),保持对象完整(319),引入参数对象(140),移出标记参数(314),函数组合成类(144)。

  5. 全局数据:全局数据的问题是在代码的任何角落都可以修改它,所以可能引入诡异的 bug,可以封装变量(132)。

  6. 可变数据:指在软件的一处更新数据,但是在另一处期望着完全不同的数据,可以通过封装变量(132)来确保更新操作都通过很少的几个函数来进行。一个数据不同时候用于存储不同数据可以用拆分变量(240),使用移动语句(230)和提炼函数(106)将更新搬到新的函数中。还有将函数和修改函数分离(306),移除设值函数(331),以查询取代派生变量(248),函数组合成类(144),函数组合成变换(149),引用对象改为值对象(252)等。

  7. 发散式变化:某一个模块经常因为不同的原因在不同的方向上变化,这就是发散式变化。这里主要是违背了单一职责原则(SRP),可以根据情况使用拆分阶段(154),搬移函数(198),提炼函数(106),提炼类(182)等。

  8. 霰弹式修改:是指每遇到某种变化,都需要在许多不同的类内做出修改。可以使用搬移函数(198),搬移字段(207),函数组合成类(144),函数组合成变换(149),拆分阶段(154),内联函数,内联类等。

  9. 依恋情节:模块化的设计都是力求高内聚,松耦合的,当一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于自己所处模块内的交流,这就是依恋情节。可以使用搬移函数(198),提炼函数(106)等。

  10. 数据泥团:数据很多时候都是结对出现的,这些总是一起出现的数据应该拥有属于它们自己的对象,可以使用提炼类(182),引入参数对象(140),保持对象完整(319)等。

  11. 基本类型偏执:编程大量使用基本类型,float,int,string 等,而不是创建对自己问题领域有用的基本类型,钱、坐标、范围等。可以运用以对象取代基本类型(174),以子类取代类型码(362),多态取代条件表达式(272),还有就是提炼类(182)和引入参数对象(140)来处理。

  12. 重复的 switch:有点类似重复代码,这里主要是在不同的地方使用同样的 switch 逻辑,这样更应该使用多态取代条件表达式(272)。

  13. 循环语句:使用管道取代循环(231)可以帮助我们更快地看清被处理的元素以及处理它们的动作。

  14. 冗赘的元素:很多类,继承或者函数完全没必要,可以使用内联函数(115),内联类(186),折叠继承体系(380)来消除。

  15. 夸夸其谈通用性:以各种各样的钩子或者特殊情况来处理一些非必要的事情,例如传入一些觉得将来可能会用到,但是现在并没用的参数,创建并不太会用到的抽象类等。可以使用内联函数(115),内联类(186),折叠继承体系(380),包括改变函数声明(124)等,

  16. 临时字段:在内部某个字段只为某种特定情况而设的,有时会让人不易理解,可以使用提炼类(182),搬移函数(198),引入特例(289)等。

  17. 过长的消息链:即客户向A请求B,然后得到B以后再向B请求 C,这就是消息链,过长的消息链会导致用户代码与查找过长的紧耦合,应该使用隐藏委托关系(189),提炼函数(106),搬移函数(198)等。

  18. 中间人:这里主要是指过度委托,某一个类的接口有一半函数都是委托给其他类,我们应该移除中间人(192),内联函数(115),以委托取代超类(399),以委托取代子类(381)等。

  19. 内幕交易:不同模块间一定的数据交换不可避免,但我们必须减少这种情况,并把这种交换放到明面上来。可以使用搬移函数(198),搬移字段(207),隐藏委托关系(189),以委托取代子类(381),以委托取代超类(399)等。

  20. 过大的类:想用一个类做太多的事情,其内往往就会有很多字段,重复代码就不可避免地出现。可以使用提炼类(182),提炼超类(182),以子类取代类型码(362)等。

  21. 异曲同工的类:使用类的好处是可以替换,但是替换的前提是它们的接口一致,可以使用改变函数声明(124),搬移函数(198),提炼超类(375)等。

  22. 纯数据类:是指只拥有一些字符,以及用于读写这些字段的函数。这样的类一定会被其他类频繁操控着,应该使用封装记录(162)将它们封装起来,对不该修改的字段移除设值函数(331),包括将其他地方的取值/设值函数都搬移函数(198),提炼函数(106),拆分阶段(154)等。

  23. 被拒绝的遗赠:子类不想或者不需要继承父类所有的函数和数据,这时意味着继承体系设计错误,就需要为这个子类新建一个兄弟类,使用函数下移(359)和字段下移(361)把用不到的函数推给新建的兄弟类。或者以委托取代子类(381)或者以委托取代超类(399)来消除不合适的继承关系。

  24. 注释:如果你需要注释一块代码做什么,可以试试提炼函数(106),改变函数声明(124),引入断言(302)等。

构筑测试体系

  1. 类应该包含它们自己的测试代码
  2. 编写自测试会写很多额外的代码,除非你确实体会到这种方法是如何提升编程速度的,否则自测试似乎就没什么意义。
  3. 编写测试最好的时机是写代码前,写测试代码也是问自己,为了添加这个功能,我们需要实现什么?
  4. 观察被测试类应该做的所有事情,然后对每个类的每个行为进行测试,包括可能发生的异常边界条件。测试是风险驱动的行为,目标是找到现在/未来可能出现的 bug。
  5. 每当收到一个 bug 报告,请先写一个单元测试来复现这个 bug
  6. 测试覆盖率只能识别未被测试覆盖到的代码,而不能衡量一个测试集的质量高低,测试集是否足够好,是一个主观的问题,自己有没有自信确认已覆盖 case。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值