(这个是在公司内部做培训内容的整理。)
各位同学,我们现在开始吧。
一个在运行的活的大系统是一个怪兽,需要大量年富力强的程序员的献祭
——大魔法师:翁一刀
之前公司我所在的部门是在线预订部。我们部门是新成立的部门,当时没什么人愿意去新的部门。因为需要学习的成本,要花大力气才能熟悉业务代码。几乎和所有公司一样,我们那边的产品经理也是很喜欢提需求。我们基本上一个产品经理对接两个App开发。我们一边开发一边重构代码,逐渐把所有业务都熟悉之后,我们捣鼓出来一套组件化开发方式。可以通过服务器配置来实现界面上的组件的任意组合。再后来听说我们部门解散了。可能是我们那套系统太好用了吧。:D我是开玩笑的。
什么是重构
重构是对软件内部结构的一种调整,目的是在不改变外部行为的前提下,提高可理解性,降低修改成本。
重构是严谨、有序地对完成的代码进行整理从而减少出错的一种方法。
在开发过程中其实我们在做两件事:
- 添加功能
- 重构
为什么重构
为什么要这么做?投入精力仅仅改变了软件的实现方式,这是否是在浪费开发资源呢?打一把王者荣耀,吃一把鸡也好呀。然而其实我们大部分的时候都会不自觉的进行代码的重构。
-
重构改进软件设计
当人们只为短期目的,或是在未完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员就会越来越难通过阅读原来来理解原来的设计。重构就像是在整理代码,你所做的就是让所有东西回到它本应该在的位置上。代码结构的流失是累积性的。越难看出代码所代表的设计意图,就越难保护其中设计,于是该设计就腐败的越快。 -
重构使软件更容易理解
我们写代码的时候,最怕的就是看别人的代码。有时候为了修改一段代码你的同事可能要花费一天,甚至几天来阅读你写的代码,事实上他如果理解了你的代码,修改起来只需要一个小时。你这个时候是不会会去想,这有什么关系呢,我也是这么被坑过来的。我这里要告诉各位同学的是,未来读你代码的人很可能是你!!! -
重构帮助找到bug
有些人可以通过调试来查找bug。也有些人直接看代码就能找到bug。对代码的理解能帮助我找到bug。对代码重构,我就可以深入理解代码的行为。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是很快就能把bug给揪出来。我不是个伟大的程序员,我只是个有着一些游戏习惯的好程序员。
——Kent Beck(https://baike.baidu.com/item/Kent%20Beck) -
重构提高编程速度
听起来有点违反直觉。当我谈到重构,人们很容易看出它能够提高质量。改善设计、提升可读性、减少错误,这些都是提高质量。难道不会降低开发速度吗?
我绝对相信:良好的设计是快速开发的根本。如果没有良好的设计,获取某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。随着补丁打的越来越多,你修改的时间也会越来越长,业务你必须花更多的时间来理解系统、寻找重复代码。如果设计不好,你就会打补丁,随着补丁增加,你的设计就会越来越复杂,这是个恶心循环。
良好的设计是维持如那件开发速度的根本。重构可以帮助你更快速地开发软件,业务它能阻止系统腐烂,它甚至还可以提高设计质量。
什么时候不应该重构
“这么烂的代码,我来重构一下!”,“这代码怎么能这么写呢?谁来重构一下?”,“这儿有个坏味道,重构吧!” 作为一名QA,每次听到“重构”两个字,既想给追求卓越代码的开发人员点个赞,同时又会感觉非常紧张,为什么又要重构?马上就要上线了,怎么还要改?
- 代码不能正常运行:折中办法,将项目拆分成一个个组件,然后对组件进行重构。
- 项目接近尾声应该避免重构。重构能够提高生产力。如果最后你没有足够时间,通常就表示你其实早该进行重构。
我们一般把未完成重构的地方称作债务。以后慢慢进行还债。
什么时候进行重构
既然重构这么重要,是不是我们每个月来安排一个星期来进行重构呢?
我是反对专门拨出时间来进行重构的。在我看来,重构本来就不是一件应该特别拨出时间做的事情,重构一个随时随地进行。你永远不应该为了重构而重构,你之所以重构,是因为你想做别的事情,而重构能帮助你把那些事情做好。
事不过三,三则重构
第一次做某件事的时候只管放手去做;
第二次做类似事情的时候,忍一忍,无论如何你还是可以去做;
第三次要做同样事情的时候,你就应该去重构了;
添加功能时重构
最常见的重构时机就是我想给软件添加特性的时候。此时,重构的最直接原因往往时为了帮助我理解需要修改的代码——这些代码可能时别人写的,也可能是我自己写的。无论如何,只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快地理解它。然后我就会重构。之所以这么做,部分原因是为了让我下次再看这段代码时容易理解,但是最主要的原因是:如果在前进过程中把带啊吗结构理清,我就可以从中理解更多东西。
还有另一个重构的原动力:代码的设计无法帮助我轻松添加所需要的特性。这个时候我们可以通过重构来改善我们原有的设计。当然这只是一部分原因,最主要的原因是我觉得这是开发最快的方式。重构是一个快速流畅的过程,一旦完成重构,新特性的添加就会更加快速,更流畅。这样能尽量的避免过度设计。过度设计会浪费过多的时间精力。过度设计会在后面介绍。
修补错误时重构
调试过程中运用重构,多半是为了让代码更具可读性。当我看着代码并努力理解它的时候,我用重构帮助加深自己的理解。我发现以这种程序来处理代码,常常能帮助我找出bug。你可以这么想:如果收到一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你一眼看出bug。
复审代码时重构
很多公司都会做常规的代码复审,因为这种活动可以改善开发状态。这种活动有助于开发团队中传播知识,也有主于让比较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。代码复审对于编写清晰代码也很重要。有时候我的代码也许对我自己来说很清晰,但是对别人则不然。这是无法避免的,因为要让开发者设身处地为那些不熟悉的人着想,实在一件非常困难的事情。代码复审也让更多人有机会提出有用的建,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到别人的帮助,是一件很愉快的事情。你应该期待更多的代码复审。
过度设计
一般来说我们都有过度设计的经验。当学会某种架构思想的时候,或者设计模式的时候,我们总是希望能第一时间用上这些新的知识和技术。然而这些东西能给我们的产品带来什么,可能只有老天爷知道了。
我们现在来看一个案例。这是一个OA系统。我们看下设计。这个系统采用前后端分离的技术,还分设计了文件管理服务,权限管理服务,通过nodejs来进行相应的服务来提供数据给前端。数据层呢,使用数据库中间件来进行操作数据库。
然而……然而……
我们来考虑一下,我们知识一个OA系统,使用的人不足100人。我们摸着良心问问自己,我们真的需要这种架构吗?
-w600
我们只需要下面这种架构足够了。除非你想练习自己新的知识。我不鼓励大家在商业产品中练习新技术。公司内部的产品,demo开发你可以尽情的去尝试。
-w00
如何重构
重构的基本技巧—小步前进、频繁测试。有一个词叫做’代码的坏味道‘,第一次看到这个词的时候感觉理解不了。现在回过头来看我觉得这个词用的很精确。一般而言我们随着自己的开发经验增加,我们的’鼻子‘会越来越灵敏,会发现更多的’坏味道‘。发现’坏味道‘时也许就是时候改进代码了。重构代码的时候我们一定要有一个验收标准,用来确保我们重构代码的时候不会引入新的问题。构建测试体系不是我们今天所讲的内容。总结一下:
- 构建完整的测试环境。
- 小步修改,频繁测试。
- 开发过程根据代码的坏味道进行代码重构。
构建测试体系
重构开始的之前一定要有完整的测试用例。否则你哪里来的自信能用来替换线上的版本呢。
- 确保所有测试都完全自动化,让他们检查自己的测试结果。
- 自动运行测试用例。
- 考虑可能出错的边界条件,把测试火力集中在那儿。
代码的坏味道
-
Duplicated Code(代码重复)
坏味道行列中首当其冲的就是重复代码。如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将他们合而为一,程序会变得更好。- 类内两个方法有重复代码,提取出一个公用方法。
- 兄弟类里面有重复的代码,提取一个公用方法,放到父类里面去。
- 两个毫无关系的类出现重复代码,考虑将代码提取到一个独立类中。
-
Long method(方法过长)
拥有短函数的对象会活的比较好,比较长。一个函数一个功能。很久以前程序员就已经认识:程序越长越难理解。我们需要更积极的分解函数。遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途来命名。关键不在于函数的长度,而在于函数的内容是否聚焦。
一般来说如果你看到注释了,你就可以考虑是不是需要把它提炼到独立函数中去。
条件表达式和循环常常也是提炼信号。
3.Large Class(过大的类)
大类就是你把太多的责任交给了一个类。这样不利于维护,因为一般来说这种情况耦合会比较严重。处理的方法一般是,从这个类中提取出新的类出来。将拥有相同名字前缀的变量提取到一个新类中。
4.Long Parameter List (过长参数列)
参数越多,后面造成变化的机会就越多。一旦参数列修改,你就要修改调用的所有地方。使用一个对象来替换参数列,比如Dictionary,KeyValue键值对。
5.Divergent Change(发散式变化)
当你看着一个类说:“如果新加入一个数据库,我必须修改这三个函数;如果新加入这种缓存,我必须修改这四个函数。”那么此时也许你将这个类分成两个比较好。针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。
6.Shotgun Surgery (霰弹式修改)
霰弹式修改类似发散式变化,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。
7.Feature Envy(依恋情结)
函数对某个类的兴趣高过对自己所处类的兴趣。通常函数会用多很多其它类的数据。无数次的经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎5个取值函数。我们需要把这个函数移到它该去的地方。
当然,并非所有情况都这么简单。一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。
-
Data Clumps(数据泥团)
有一些数据总是一起出现,你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
我们可以把这些数据找出来,删掉其中的一项看剩下来的是否有意义,如果没有意义我们就该把它们放到一个新类中。 -
Primitive Obsession(基本类型偏执)
-
Swich Statements
-
Parallel Inheritance Hierarchies(平行继承体系)
-
Lazy Class(冗余类)
-
Speculative Generality(夸夸其谈未来性)
这个令我们十分敏感的坏味道,命名者是Braian Foote。当有人说“我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有功能都会被用到,那就是值得那么做;如果用不到,就不值得。用不到的功能只会挡住你的路,所以把它搬开吧。 -
Temporary Field
提取到相应函数中 -
Message Chains
-
Middle Man
过度委托 -
Inappropriate Intimacy(不合适的关系)
拆散它们!!! -
Alternative Classes with Different Interfaces(异曲同工的类)
-
Incomplete Library Class(不完美的类库)
-
Data Class(数据类)
-
Refused Bequest(被拒绝的遗赠)
从父类集成的数据和方法不需要。新建一个兄弟类,然后把不需要的数据和方法移到兄弟类中。然后把需要的数据和方法放到父类中。 -
Comments(过多的注释)
例子
实例非常简单。这是一个影片出租店应用的程序,计算每一个顾客的消费金额并打印清单。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用还要为常客计算积分,积分会更具影片是否是新片而有不同。
开始设计:(main.py)
-w600
-w600 -w600
我们这个例子比较简单,你想怎么写都行。我们先粗略的看一下,你会看到statement这个函数做了太多的事情。这就是坏味道,这个很多时候只可意会不可言传。
接下来我们有一个需求,希望输出的是HTML格式,而非纯文本。
为了实现这个新的需求,我们先对statement这个函数来进行重构。重构是为了更好的实现该功能。
-
提取计算金额的函数(main1.py)
-w600 -
修改变量名,增加可读性(main1.py)
image -
转移计算金额函数
-w600
现在整个程序变成了下面的样子。
-w600
-
移除临时变量,因为它会降低代码可读性,同时不利于重构
-w600
接下来我们来处理积分问题
-
提取积分函数
-w600 -
转移积分函数
-w600
下面图是修改前后的对比。
-w600
-
移除临时变量
-w600
-w600
-
至此我们只需要新建一个html_statement函数就解决问题了。
-w600
接下来产品经理告诉我们他准备修改规则,但是还没想好。为了对付他,我们来继续重构吧。我们观察Rental类里面的amount使用到了Movie中的数据。我们把amount转移到Movie中。
-
转移amount函数
-w600
同样原理转移frequent_renter_points()
之后的类如对比如下。
-w600
- 接下来我们通过多态来解偶
amount()
-w600
但是到现在为止我们还是没有消除if
语句。
-
移除计算金额中的
-w600if
语句
-
同样手法来处理
-w600frequent_renter_points()
总结
- 重构是为了更好的开发。
- 构建完整的测试环境很重要。
- 如果重构解决不了问题,就需要重新设计。
- 经验很重要,重构解决不了设计问题。
网友评论