重构之从入门到放弃

作者: iHugo | 来源:发表于2018-06-20 17:55 被阅读17次

    (这个是在公司内部做培训内容的整理。)

    各位同学,我们现在开始吧。

    一个在运行的活的大系统是一个怪兽,需要大量年富力强的程序员的献祭
    ——大魔法师:翁一刀

    之前公司我所在的部门是在线预订部。我们部门是新成立的部门,当时没什么人愿意去新的部门。因为需要学习的成本,要花大力气才能熟悉业务代码。几乎和所有公司一样,我们那边的产品经理也是很喜欢提需求。我们基本上一个产品经理对接两个App开发。我们一边开发一边重构代码,逐渐把所有业务都熟悉之后,我们捣鼓出来一套组件化开发方式。可以通过服务器配置来实现界面上的组件的任意组合。再后来听说我们部门解散了。可能是我们那套系统太好用了吧。:D我是开玩笑的。

    什么是重构

    重构是对软件内部结构的一种调整,目的是在不改变外部行为的前提下,提高可理解性,降低修改成本。
    重构是严谨、有序地对完成的代码进行整理从而减少出错的一种方法。

    在开发过程中其实我们在做两件事:

    1. 添加功能
    2. 重构

    为什么重构

    为什么要这么做?投入精力仅仅改变了软件的实现方式,这是否是在浪费开发资源呢?打一把王者荣耀,吃一把鸡也好呀。然而其实我们大部分的时候都会不自觉的进行代码的重构。

    1. 重构改进软件设计
      当人们只为短期目的,或是在未完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员就会越来越难通过阅读原来来理解原来的设计。重构就像是在整理代码,你所做的就是让所有东西回到它本应该在的位置上。代码结构的流失是累积性的。越难看出代码所代表的设计意图,就越难保护其中设计,于是该设计就腐败的越快。

    2. 重构使软件更容易理解
      我们写代码的时候,最怕的就是看别人的代码。有时候为了修改一段代码你的同事可能要花费一天,甚至几天来阅读你写的代码,事实上他如果理解了你的代码,修改起来只需要一个小时。你这个时候是不会会去想,这有什么关系呢,我也是这么被坑过来的。我这里要告诉各位同学的是,未来读你代码的人很可能是你!!!

    3. 重构帮助找到bug
      有些人可以通过调试来查找bug。也有些人直接看代码就能找到bug。对代码的理解能帮助我找到bug。对代码重构,我就可以深入理解代码的行为。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是很快就能把bug给揪出来。

      我不是个伟大的程序员,我只是个有着一些游戏习惯的好程序员。
      ——Kent Beck(https://baike.baidu.com/item/Kent%20Beck

    4. 重构提高编程速度
      听起来有点违反直觉。当我谈到重构,人们很容易看出它能够提高质量。改善设计、提升可读性、减少错误,这些都是提高质量。难道不会降低开发速度吗?
      我绝对相信:良好的设计是快速开发的根本。如果没有良好的设计,获取某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。随着补丁打的越来越多,你修改的时间也会越来越长,业务你必须花更多的时间来理解系统、寻找重复代码。如果设计不好,你就会打补丁,随着补丁增加,你的设计就会越来越复杂,这是个恶心循环。
      良好的设计是维持如那件开发速度的根本。重构可以帮助你更快速地开发软件,业务它能阻止系统腐烂,它甚至还可以提高设计质量。

    什么时候不应该重构

    “这么烂的代码,我来重构一下!”,“这代码怎么能这么写呢?谁来重构一下?”,“这儿有个坏味道,重构吧!” 作为一名QA,每次听到“重构”两个字,既想给追求卓越代码的开发人员点个赞,同时又会感觉非常紧张,为什么又要重构?马上就要上线了,怎么还要改?

    1. 代码不能正常运行:折中办法,将项目拆分成一个个组件,然后对组件进行重构。
    2. 项目接近尾声应该避免重构。重构能够提高生产力。如果最后你没有足够时间,通常就表示你其实早该进行重构。

    我们一般把未完成重构的地方称作债务。以后慢慢进行还债。

    什么时候进行重构

    既然重构这么重要,是不是我们每个月来安排一个星期来进行重构呢?

    我是反对专门拨出时间来进行重构的。在我看来,重构本来就不是一件应该特别拨出时间做的事情,重构一个随时随地进行。你永远不应该为了重构而重构,你之所以重构,是因为你想做别的事情,而重构能帮助你把那些事情做好。

    事不过三,三则重构

    第一次做某件事的时候只管放手去做;
    第二次做类似事情的时候,忍一忍,无论如何你还是可以去做;
    第三次要做同样事情的时候,你就应该去重构了;

    添加功能时重构

    最常见的重构时机就是我想给软件添加特性的时候。此时,重构的最直接原因往往时为了帮助我理解需要修改的代码——这些代码可能时别人写的,也可能是我自己写的。无论如何,只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快地理解它。然后我就会重构。之所以这么做,部分原因是为了让我下次再看这段代码时容易理解,但是最主要的原因是:如果在前进过程中把带啊吗结构理清,我就可以从中理解更多东西。
    还有另一个重构的原动力:代码的设计无法帮助我轻松添加所需要的特性。这个时候我们可以通过重构来改善我们原有的设计。当然这只是一部分原因,最主要的原因是我觉得这是开发最快的方式。重构是一个快速流畅的过程,一旦完成重构,新特性的添加就会更加快速,更流畅。这样能尽量的避免过度设计。过度设计会浪费过多的时间精力。过度设计会在后面介绍。

    修补错误时重构

    调试过程中运用重构,多半是为了让代码更具可读性。当我看着代码并努力理解它的时候,我用重构帮助加深自己的理解。我发现以这种程序来处理代码,常常能帮助我找出bug。你可以这么想:如果收到一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你一眼看出bug。

    复审代码时重构

    很多公司都会做常规的代码复审,因为这种活动可以改善开发状态。这种活动有助于开发团队中传播知识,也有主于让比较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。代码复审对于编写清晰代码也很重要。有时候我的代码也许对我自己来说很清晰,但是对别人则不然。这是无法避免的,因为要让开发者设身处地为那些不熟悉的人着想,实在一件非常困难的事情。代码复审也让更多人有机会提出有用的建,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到别人的帮助,是一件很愉快的事情。你应该期待更多的代码复审。

    过度设计

    一般来说我们都有过度设计的经验。当学会某种架构思想的时候,或者设计模式的时候,我们总是希望能第一时间用上这些新的知识和技术。然而这些东西能给我们的产品带来什么,可能只有老天爷知道了。
    我们现在来看一个案例。这是一个OA系统。我们看下设计。这个系统采用前后端分离的技术,还分设计了文件管理服务,权限管理服务,通过nodejs来进行相应的服务来提供数据给前端。数据层呢,使用数据库中间件来进行操作数据库。
    然而……然而……
    我们来考虑一下,我们知识一个OA系统,使用的人不足100人。我们摸着良心问问自己,我们真的需要这种架构吗?


    -w600

    我们只需要下面这种架构足够了。除非你想练习自己新的知识。我不鼓励大家在商业产品中练习新技术。公司内部的产品,demo开发你可以尽情的去尝试。


    -w00

    如何重构

    重构的基本技巧—小步前进、频繁测试。有一个词叫做’代码的坏味道‘,第一次看到这个词的时候感觉理解不了。现在回过头来看我觉得这个词用的很精确。一般而言我们随着自己的开发经验增加,我们的’鼻子‘会越来越灵敏,会发现更多的’坏味道‘。发现’坏味道‘时也许就是时候改进代码了。重构代码的时候我们一定要有一个验收标准,用来确保我们重构代码的时候不会引入新的问题。构建测试体系不是我们今天所讲的内容。总结一下:

    1. 构建完整的测试环境。
    2. 小步修改,频繁测试。
    3. 开发过程根据代码的坏味道进行代码重构。

    构建测试体系

    重构开始的之前一定要有完整的测试用例。否则你哪里来的自信能用来替换线上的版本呢。

    1. 确保所有测试都完全自动化,让他们检查自己的测试结果。
    2. 自动运行测试用例。
    3. 考虑可能出错的边界条件,把测试火力集中在那儿。

    代码的坏味道

    1. Duplicated Code(代码重复)
      坏味道行列中首当其冲的就是重复代码。如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将他们合而为一,程序会变得更好。

      1. 类内两个方法有重复代码,提取出一个公用方法。
      2. 兄弟类里面有重复的代码,提取一个公用方法,放到父类里面去。
      3. 两个毫无关系的类出现重复代码,考虑将代码提取到一个独立类中。
    2. Long method(方法过长)
      拥有短函数的对象会活的比较好,比较长。一个函数一个功能。很久以前程序员就已经认识:程序越长越难理解。我们需要更积极的分解函数。遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途来命名。关键不在于函数的长度,而在于函数的内容是否聚焦。
      一般来说如果你看到注释了,你就可以考虑是不是需要把它提炼到独立函数中去。
      条件表达式和循环常常也是提炼信号。

    3.Large Class(过大的类)
    大类就是你把太多的责任交给了一个类。这样不利于维护,因为一般来说这种情况耦合会比较严重。处理的方法一般是,从这个类中提取出新的类出来。将拥有相同名字前缀的变量提取到一个新类中。

    4.Long Parameter List (过长参数列)
    参数越多,后面造成变化的机会就越多。一旦参数列修改,你就要修改调用的所有地方。使用一个对象来替换参数列,比如Dictionary,KeyValue键值对。

    5.Divergent Change(发散式变化)
    当你看着一个类说:“如果新加入一个数据库,我必须修改这三个函数;如果新加入这种缓存,我必须修改这四个函数。”那么此时也许你将这个类分成两个比较好。针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。

    6.Shotgun Surgery (霰弹式修改)
    霰弹式修改类似发散式变化,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。

    7.Feature Envy(依恋情结)
    函数对某个类的兴趣高过对自己所处类的兴趣。通常函数会用多很多其它类的数据。无数次的经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎5个取值函数。我们需要把这个函数移到它该去的地方。
    当然,并非所有情况都这么简单。一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。

    1. Data Clumps(数据泥团)
      有一些数据总是一起出现,你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
      我们可以把这些数据找出来,删掉其中的一项看剩下来的是否有意义,如果没有意义我们就该把它们放到一个新类中。

    2. Primitive Obsession(基本类型偏执)

    3. Swich Statements

    4. Parallel Inheritance Hierarchies(平行继承体系)

    5. Lazy Class(冗余类)

    6. Speculative Generality(夸夸其谈未来性)
      这个令我们十分敏感的坏味道,命名者是Braian Foote。当有人说“我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有功能都会被用到,那就是值得那么做;如果用不到,就不值得。用不到的功能只会挡住你的路,所以把它搬开吧。

    7. Temporary Field
      提取到相应函数中

    8. Message Chains

    9. Middle Man
      过度委托

    10. Inappropriate Intimacy(不合适的关系)
      拆散它们!!!

    11. Alternative Classes with Different Interfaces(异曲同工的类)

    12. Incomplete Library Class(不完美的类库)

    13. Data Class(数据类)

    14. Refused Bequest(被拒绝的遗赠)
      从父类集成的数据和方法不需要。新建一个兄弟类,然后把不需要的数据和方法移到兄弟类中。然后把需要的数据和方法放到父类中。

    15. Comments(过多的注释)

    例子

    实例非常简单。这是一个影片出租店应用的程序,计算每一个顾客的消费金额并打印清单。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用还要为常客计算积分,积分会更具影片是否是新片而有不同。

    开始设计:(main.py)


    -w600
    -w600 -w600

    我们这个例子比较简单,你想怎么写都行。我们先粗略的看一下,你会看到statement这个函数做了太多的事情。这就是坏味道,这个很多时候只可意会不可言传。
    接下来我们有一个需求,希望输出的是HTML格式,而非纯文本。
    为了实现这个新的需求,我们先对statement这个函数来进行重构。重构是为了更好的实现该功能。

    1. 提取计算金额的函数(main1.py)


      -w600
    2. 修改变量名,增加可读性(main1.py)


      image
    3. 转移计算金额函数


      -w600

    现在整个程序变成了下面的样子。


    -w600
    1. 移除临时变量,因为它会降低代码可读性,同时不利于重构


      -w600

    接下来我们来处理积分问题

    1. 提取积分函数


      -w600
    2. 转移积分函数


      -w600

    下面图是修改前后的对比。


    -w600
    1. 移除临时变量


      -w600
      -w600
    -w600
    1. 至此我们只需要新建一个html_statement函数就解决问题了。


      -w600

    接下来产品经理告诉我们他准备修改规则,但是还没想好。为了对付他,我们来继续重构吧。我们观察Rental类里面的amount使用到了Movie中的数据。我们把amount转移到Movie中。

    1. 转移amount函数


      -w600

    同样原理转移frequent_renter_points()之后的类如对比如下。

    -w600
    -w600
    1. 接下来我们通过多态来解偶amount()
      -w600
    image

    但是到现在为止我们还是没有消除if语句。

    1. 移除计算金额中的if语句

      -w600
    2. 同样手法来处理frequent_renter_points()

      -w600

    总结

    1. 重构是为了更好的开发。
    2. 构建完整的测试环境很重要。
    3. 如果重构解决不了问题,就需要重新设计。
    4. 经验很重要,重构解决不了设计问题。

    相关文章

      网友评论

        本文标题:重构之从入门到放弃

        本文链接:https://www.haomeiwen.com/subject/yeixyftx.html