上篇文章介绍了 TDD,这次我们将极限编程中的所有技术实践合起来一起聊聊。
重构
为了统一语言,我想有必要在开始讲重构前聊聊到底什么是重构。很多人讲到重构时甚至讲的是“将已有代码全删掉,重新写一遍这件事”,很显然这是重写不叫重构。
重构是改善代码结构的一种实践,但重构并不会改变由测试定义的行为。
重构应该是在不破坏任何测试的前提下对命名、类、函数和表达式进行修改。在不影响行为逻辑的情况下改善系统的结构。
发现了么,重构需要完备的测试做安全网,这层安全网能给你提供重构的信心和勇气。而 TDD 又能够提供重构所需的完备测试,这又是一项 TDD 优于后补测试的优点。TDD 和重构密不可分,所以有了
红-绿-重构
没错,又是这张图。上篇文章讲 TDD 时我用了这张图,但是刻意避开了蓝色的重构部分,现在是时候把二者结合起来了。一个完整的“红-绿-重构”循环应该是这样的:
- 创建一个失败的测试。
- 写出恰好能使测试通过的生产代码。
- 重构你刚刚写出的代码。
- 回到第一步。
“红-绿-重构”将写代码这件事分成了两个部分 - 编写可用的代码和编写整洁的代码,为什么需要拆开呢?
对于几乎所有人来说,光是编写可用的代码就已经很困难了,这个过程中我们需要不断调整,不断试错,更别说编写整洁可用的代码。因此,“红-绿-重构”将这两个部分拆开,先以脏乱的代码将我们脑子里的想法表达出来,一旦这些代码通过了你刚刚写下的测试,我们就开始重构刚刚写下的代码。
很显然,这是一个持续的循环过程,而不是一个定期发生的事情。也就是说,我们不应该先写一大堆实现,然后代码慢慢腐化,这个时候你才说,我应该重构一下这堆代码。你应该时时刻刻重构你一分钟前刚写下的代码。等你完成任务时,你的代码就是简洁可用的。
重构不应该是单独拿出来花时间做的一件事情,也不应该出现在项目的计划中。重构应该是日常开发中时时刻刻都在进行的活动,它就是开发活动中不可分割的一部分。
大型重构
这个时候想必有人会提出疑问:如果我发现系统当前的设计和架构无法支撑某一个需求的变更,我需要对系统进行一次大型重构该怎么办?
首先我们需要达成一个共识 - 系统架构不是一成不变的,是可以逐渐演进的。逐渐演进就意味着架构不会一次性从某个设计变成另一个设计。
所以这样的大型重构依然应该按照“红-绿-重构”的节奏来进行。既然现在的设计和架构无法支撑新的需求,那么就先重构一部分架构使其能够支持新的需求,然后添加新的部分需求功能。
在此期间,新的需求不断被实现,设计和架构也在不断被修改,这个过程可能是几天、几个星期甚至是几个月的时间。但是即使此时修改还没有完成,我们依然有信心在任何时候部署我们的代码,因为所有测试依然是通过的。
简单设计
简单设计指的是:仅编写必要的代码,使得程序结构保持最简单、最小和最富表现力。简单设计是重构的目标之一。其规则如下:
- 所有测试通过。
- 揭示意图。
- 消除重复。
- 减少元素。
这里的需要既是执行顺序又是优先级,也就是说在写代码时应该优先满足上方的规则,然后在不破坏上方规则的前提下满足下面的规则。接下来我们一条条解释:
- 这条想必不用过多解释,代码必须能工作,这是最低要求。
- 代码工作起来后,不应该是一堆看不懂的字符。你写出的代码应该能表达你的思想,揭示你写这段代码的意图。也就是说你的代码应该易于阅读并能自我表达。要想做到这一点,拆分函数,表意的命名必不可少。
- 在具备上述两点后,我们应该找出和消除代码中的所有重复内容,毕竟重复可能是程序员最不能忍的问题了。此时的重构可能比起上面会难一些,大多数时候我们只需要抽出重复内容并调用即可,但是复杂情况我们可能需要使用一些设计模式来解决。
- 消除重复后,我们应该尽可能减少元素,比如类、函数、变量等等。
简单设计的目的和名字一样简单 - 尽量降低代码的设计重量,增加代码的可维护性。毕竟代码写出来是给人看的。
结对编程
这又是一个争议颇多的实践 - 两人(或更多人)共同解决同一编程问题。结对的成员工作在同一台电脑上,共享屏幕、键盘和鼠标,当然也有许多工具支撑远程结对(比如 Intellij 的 code with me 插件)。
结对时有不同的角色。
在“驾驶员和导航员模式”中,“驾驶员”控制鼠标键盘,“导航员”则负责观察和及时给出建议;在“乒乓模式”中一个人先编写测试,另一个人让测试通过后编写下一个测试,再让第一个人编写实现,如此反复;当然还有老人写和讲,新人听和问的模式,这通常为了帮助新人快速熟悉代码库。
那么这么多模式,是不是结对的时候一定要遵循某一种模式呢?当然不是,大多数情况的结对其实并没有角色的划分。两者平等,合作解决问题。
相比起其他技术实践来说,结对是可选的,管理者不应以任何形式要求成员强制结对,有很多理由支撑独立写代码这件事。同时结对应该是间歇性的,团队内的成员应该有一段时间在结对,至于多久,不重要,这取决于个人和团队。
对于资深程序员来说,与初学者结对的次数应该超过与其他资深程序员结对的次数。对于初学者来说,与资深程序员的结对次数应该多于与其他初学者的次数。具备特殊技能的程序员应该经常与不具备特殊技能的程序员一起结对工作,使知识在各个成员间传播和交换。
为什么要结对
结对是团队成员之间共享知识并防止形成知识孤岛的最佳方法,这确保了团队中没有人是不可或缺的,此时如果一个人倒下,可以有别的人立马代替他的位置,继续向着目标前进。
同时结对可以减少错误并提高设计质量,两人在解决问题的过程中会有无数次讨论,两个人同时聚焦在同一个问题上。基于这个原因,已经有很多团队以结对的方式代替了代码评审(code review)。
代价
这也是管理者们最关心的问题,我出了两个人的钱,你们却坐在一起做同一份事情?结对确实需要付出一些人力成本,并且这些成本难以估量,据 Bob 大叔的描述,研究表明结对的成本大概是 15%,也就是说如果结对,则需要 115 人来完成不结对时 100 人的工作量。
大家能轻而易举的看到结对的代价,却很难聚焦于结对带来的好处,因为结对带来的好处的确很难量化,也不易被发现,只有去实践了,你才能真正认识到它们。但遗憾的是,大多数人连尝试的机会都被剥夺了。
再谈极限编程
从两篇文章可以看出,极限编程的几个技术实践是相辅相成、缺一不可的。TDD、重构、简单设计,无论缺了哪一个,你的代码都有可能慢慢腐化,等到所有人都发现已经没办法再往代码库里添加新功能时,重写整个系统就又会提上议程,然后再次陷入无止境的循环。
而结对编程在其中又处于一个特殊的位置,它所能带来的好处其实也是不可或缺的,但由于种种原因又是比较难实现的一种实践。结对所带来的知识共享和代码质量如果你想做到可能得花费更大的力气。
敏捷的技术实践是任何敏捷工作中最本质的组成部分,是敏捷的核心。任何敏捷实践的导入,如果没有包含技术实践,都注定会失败。没有保持高技术质量的技术实践,团队的生产力将快速下降,最终陷入不可避免的重写循环。