1 如果不做重构
如果按一般的做法,完成功能后,代码可能会长这个样子:
class CaloryCalculator {
calculate(sex: Sex, age: number, weight: number): number {
if (age <= 0) {
throw new Error("invalid age");
}
if (sex == Sex.male) {
if (age < 18) {
return 380 + 14.1 * weight;
} else if (age >= 18 && age <= 30) {
return 680 + 15.2 * weight;
} else if (age > 30 && age <= 60) {
return 830 + 11.5 * weight;
} else if (age > 60) {
return 490 + 13.4 * weight;
} else {
return 0;
}
} else {
if (age < 18) {
return 320 + 13.2 * weight;
} else if (age >= 18 && age <= 30) {
return 450 + 14.6 * weight;
} else if (age > 30 && age <= 60) {
return 830 + 8.6 * weight;
} else if (age > 60) {
return 600 + 10.4 * weight;
} else {
return 0;
}
}
}
}
我们可能会认为这没什么大不了的,相反还会觉得代码很直观,但这只是功能的堆砌而已,未来需要再加多几个计算维度,那将是一场灾难,我们能想像一下在这份代码基础上添加上诸如:职业,居住地等维度后会变成什么样子吗?if与else齐飞,switch共case一色,那景色不要太美。。。
再回顾一下我们的需求文档 https://gitee.com/JasonLiSz/TypescriptTesting/blob/feature/TDD_CaloryCalculator/Requirement.txt 目前这份代码所实现的功能,只不过是各方因素的妥协,并非是产品经理心目中理想的结果,也就是临时方案。
那我们能否先让它先这样,等未来有新需求或需求变得完整后再来处理呢?或者等需求明确后重写呢?我觉得这种做法并不好,如果因为现在没有时间,那留待未来再解决时的代价将会变得更大。至于说重写,只要能重构,就不应该考虑重写,因为需求也是在不断演化的,我们很难碰到需求一步到位的情况,需求与交付在这个迭代中是妥协的结果,那下一个迭代仍然可能会是类似的情况,也就是说这是产品经理平衡各个功能开发的投入/产出(ROI)的结果,而作为开发,我们能做的就是:让自己的功能代码尽可能地适应变化,能够有这个能力,在原有功能的基础上,让我们的软件架构随需求而演化。
如何做到这一点呢?这就涉及到我们今天所讲的主题——重构。
2 问重构为何物
先看下重构的定义:重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
我觉得在定义中应该加上前题条件,也就是:在不改变软件的外在行为的前提条件下,优化代码的结构。
重构的终极目标是让代码能够适应需求的变化,提升软件的生命力,而不注重在研发新功能时进行重构的代码,常见的表象(或者症状)就是:代码改不下去,原来加一个相似复杂度的需求,可能需要2小时,再加一个,需要半天,然后其代价不断增加,并且陷入无穷不尽的缺陷,按下葫芦浮起瓢。
而重构就是如何让软件架构随需求演化这一问题的答案,重构也是TDD中非常重要的一环,是软件可持续发展(sustainability)的必要条件。
另外一个很重要的概念,也往往是被忽视的概念,那就是如何控制重构的进程,做到随时开始,随时结束。常常听到某项目组说,花费若干个月做一个巨大的重构,听起来应该是在重写而非重构,我们在上面讲过:只要能重构,就不要重写。Why?以我对大家的了解,肯定在这里会问为什么?这个功能都维护不下去了,为什么不让我重写,在回答这个问题之前,让我先问几个问题:
- 什么样的功能模块会让我们难以维护,进而感觉重构无从下手?
这个问题简单啊,“被人翻来覆去改过的代码呗”,而且是被很多人“操练”过的代码。
嗯,很好啊,我的第2个问题来了
- 什么样的代码会被很多人翻来覆去地操练?
嗯,这个问题要稍稍想个两秒,但不至于难以回答,“应该是功能变化较快的代码吧”。
不错,快接近真相了,我的第3个问题是
- 什么样的功能模块会变频繁发生变化?
怎么样?是不是不太好回答了?因为这些功能模块对应的需求本身就在不断发生频繁的变化,而这些需求(或需求集合)所对应的功能模块,极有可能是系统的核心功能模块
如果不幸被我言中,那么请问一下自己,我们能接受核心功能模块重写所带来的业务风险吗?如果能,那么重构真的没那么重要,请出门左转,慢走,不送。
3 重构要如何做
这是一个很大的问题,如何才能达成不改变外在行为的优化呢?如何才能控制重构的进程呢?这需要长期的不断练习,这里从重构的步骤上列出了3个要点:
- 识别坏味道
- 制定重构计划
- 控制重构范围
识别坏味道,我们只有认识到问题,才有可能改进,最可怕的就是自我感觉良好,认为我们的代码没啥问题,日复一日都是这样写法,那只是做项目,与搬砖无异。如何识别坏味道,我们会花一堂课时间专门讲解。
制定重构计划,这里的计划包含:重构的目标,重构的代价,重构的步骤与计划。
重构的目标很重要,我们在重构之前需要有清醒的认识,这次重构我要达成什么,为了达成这个目标,我需要付出多少代价。
就算是一个小小的重构,也最好不要拿起来就改,花1~2分钟先想一下目标与计划也是很有帮助的,能够训练出良好的习惯,这里的计划也并非是完备设计图,稍复杂的重构,画个草图一般也就够了。
确定好重构的目标然后细化重构的步骤与计划,如:分多少步完成,每一小步的目标是什么,划分步骤并非是一件简单的事情,反而相当重要,这直接关系到第3步控制重构范围。
控制重构范围是贯穿于整个重构过程中的,这里所讲的范围包括代码范围(影响多少个模块,多少个类,多少个方法),还包括业务范围(影响哪些业务、功能点)。如果不能控制好重构的范围,一会儿觉得这里要改改,那里也要改,越改越多,越改越没思路,很容易在迷失了方向,越改越混乱,签入代码觉得不好,回退又不甘心,进退两难。我们只有在事前做好规划,才能像切牛扒一样,将复杂的重构切分成一个个可控的目标,每完成一个小目标,就可以执行代码签入,缩短反馈回路,用一个个短小的反馈环拆分一个巨大无比的重构任务,也唯有如此,才能做到随时开始,随时结束
剩下的就是重复这3个过程,不断练习,不断总结(做好PDCA),提升重构的能力。
4 为什么要随时开始随时结束
回答这个问题之前,我们想一想,我们有没有一个完整的迭代基本上啥都不干,就做重构?恐怕是没有的,如果代码坏到一定程度,写不下去了,可能项目组会不得不拿出一定的时间对原有代码进行所谓的重构(虽然在我看来,大部分情况下仍然是在做重写),但是如果开发组没有真正认识到重构的重要性,不锻炼出重构的能力,那过不了多久,又会进入另一个因改不下去而停下来重构的循环,软件生命周期越长,越是如此,最后会有人跳出来说:改不下去了,不重写一个,我就不干了。。。
没有哪个开发愿意维护别人的烂代码
好的,回到主题,所谓的随时开始,随时结束,就是:在重构的进程中,随时可以从中脱身转战更重要的任务,而重构的中间成果可以在少于1分钟的时间签入代码库,以便后续,甚至是其他团队成员接手。要做到这一点,那就必须尽可能地缩减代码不可运行的时间段(包括编译失败,功能异常)。
为什么要随时开始,随时结束?这是因为,如果我们一次要改动太多的代码,在重构过程中,来了另一个更重要的任务,比如生产问题,这时请问我改了一半的代码是签入还是不签入?如果签入,则不符合我们的DoD(完成的定义),而且当时的代码极有可能是无法正常运行的,但如果不签入,又太不甘心。。。怎么办?保存在一个新文件夹中,等下次有时间再改?程序员十大谎言,等我有时间再改。。。
再则,那个更重要的任务持续时间是不确定的,可能是10 分钟,也可能是几天,再回过头来,我们的思路可能就断了,更可怕的是,可能代码库中相关的模块已发生了变更,合并代码又成了一个麻烦事。如果不能有意识地进行随时开始,随时结束的训练,在未来的重构任务中,就有可能令自己去到进退两难的境地。
另一方面,如果我们能做到随时开始,随时结束,也就意味着我们的重构规划做到了位,我们才能控制好重构的范围与进程,每一步都很清楚知道要做什么,达成什么目标,当我们陷入其他更重要的任务短时不能脱身时,我们可以很容易地将我们的重构计划及完成进度转交给其他同事继续进行。
5 重构不要与开发新功能混在一起
请不要过高地估计了我们同时做多件不同事情的能力,我所认识的人中除了一个人以外,绝大部分人都是不能做到多线程处理的,同时处理多件事情的结果就是丢三落四,最终一件都做不好。
重构与新功能不要混在一起,就是针对于这个情况,开发往往会高估自己,在开发新功能时,觉得这里代码不好,然后就顺手改一改,这样做,不但容易在切换时丢失信息,而且在功能开发过程中一但发现之前所做的重构不是最优的,或者功能开发过程中觉得需求理解有问题,或者需求本身有问题,请问这个代码回退不回退?怎样做都不好,所以在开发过程中,要忍得住,开发新功能就开发新功能,重构就是重构,分开进行,开发新功能也需要切分成细粒度一步步签入,如果做新功能时觉得要重构,那就完成这一小部分功能,签入后,再做重构,重构完成后,签入,再继续开发下一个功能点,做好规划才能不混乱,再三强调,永远不要高估自己的多线程能力。
原始代码与需求参考文档:
https://gitee.com/JasonLiSz/TypescriptTesting/tree/feature/TDD_CaloryCalculator
同学们重构后的代码如下:
https://gitee.com/BruceZhang2018/tdd_reconstruction/tree/master
网友评论