《修改代码的艺术》,英文名《Working Effectively with Legacy Code》,中文翻译的文笔上绝对谈不上“艺术”二字,愧对艺术二字(当然译者不是这个意思)。书中第三部分不论是例子还是解说都有点混乱,远不如《重构——改善既有代码设计》一书。此书精华在于第一、二部分。
如何学习这本书,作为一个最底层的码农,作为长期在别人代码上修修补补的苦逼二手货开发人员,我只能给的建议就是:你可以将它看做是如何做定制功能的指导书——从某种意义上讲,很多时候引入测试,实际就是添加一个叫做“测试”的定制功能。而且,这样似乎也恰好印证了该书的中文名”修改代码的艺术”。
其他的,我不想谈,也不懂。就这样。
既然是要将这本书看做是如何做定制功能的指导书,那么就先从本书第二部分“修改代码的技术”开始看。
1. 降低修改的风险
- 好的代码编辑工具。
哪些把二进制数据也能玩得出神入化的大牛,我就不考虑了。对像我这样的普通猿类,没能吃上ALZ112,更别提ALZ113了,智商有限。只能用工具补拙了。吐槽一下我们公司竟然基本都在用source insight,这个工具中文编码支持又不好,搜索和补全命令不强,每次都只能呵呵。
哪些把二进制数据也能玩得出神入化的大牛,我就不考虑了。对像我这样的普通猿类,没能吃上ALZ112,更别提ALZ113了,智商有限。只能用工具补拙了。吐槽一下我们公司竟然基本都在用source insight,这个工具中文编码支持又不好,搜索和补全命令不强,每次都只能呵呵。
- 单一目标的编辑。
这个在重构一书中也反复强调了。这里个人的体会是,老老实实的遵循吧,当习惯成自然了,进步的时机就到了。别以为学了高量和相对论,经典力学就能随便玩了。
这个在重构一书中也反复强调了。这里个人的体会是,老老实实的遵循吧,当习惯成自然了,进步的时机就到了。别以为学了高量和相对论,经典力学就能随便玩了。
2. 需要修改大量相同的代码
对修改相同代码,我近乎偏执,原因也许就是源于下面两句作者的话:
- 当你热情地消除代码中的重复时,就会惊讶地发现,设计会自己浮现出来。
- 消除重复是锤炼设计的强大手段,它不仅能让设计变得更灵活,还能令代码修改更快更容易。
这里补充一点的是相同的代码,不一定是完全相同的代码。有时出现完全相同的代码,只是因为一种巧合;很多时候,碰到最多的是结构和逻辑上相似的重复代码。
3. 时间紧迫,必须修改
不管时间是否紧迫,作为一种自我保护的本能,一般修改时,尽可能将修改的代码集中到单独的类或者方法中,实现上尽可能的是类似一种开关性质的,可以简单的enable or disable。
但是,如果时间充裕,应该在可测的情况下进行可能的重构,我自己的感受是有时这种自我保护的本能太过强烈,有些时候会有些畸形,这样写出来的代码也许是最安全的,但不是最优雅的。就像很多贪心算法,不总是最优,但往往还够得上"优"
这里借用原书的例子讨论
原始的代码,对列表entries中的每个对象,依次执行postDate()对象,然后添加的transactionBundle的管理池中。
public void postEntries(List<Entry> entries) { for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entries); }
现在有个新的需求,需求描述是这样的:(需求描述实际是很关键的,不同的描述方式会不自觉的影响程序员的实现方式)
entries列表中不是所有的对象都要执行postDate()和添加进transactionBundle的管理池中。只有还尚未在transactionBundle的管理池中的对象才需要执行postDate()操作,只有那些执行了postDate()的entry对象,才需要添加到transactionBundle的管理池中。
根据上面的需求描述,如果你是那99.9%的人,一般就会这样实现:
public voidpostEntries(List<Entry> entries) { // 记录哪些entries中哪些对象执行了postDate() List<Entry>entriesToAdd = newLinkedList<Entry>(); for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); // 只有那些不在transactionBundle管理池中的entry对象才需要执行postDate() if (!transactionBundle.getListManager().hasEntry(entry)) { entry.postDate(); entriesToAdd.add(entry); } } // 将那些执行了postDate的entry对象添加到transactionBundle管理池中 transactionBundle.getListManager().add(entriesToAdd); }
无疑,这样的修改非常具有侵入性,一旦出错,很难定位是本身已有的缺陷还是改动造成的——只有在深入理解代码的改动逻辑之后才能分析错误原因。这个不好。
这个需求,本质上就是先找出那些还没有在管理池中的entry对象,然后执行postDate()和add()操作。因此这里实际可以应用“新生方法”手法,引入一个侵入性相当弱的修改。
public voidpostEntries(List<Entry> entries) { // 先剔除那些已经在transactionBundle管理池中的entry对象 List<Entry> entriesToAdd =uniqueEntries(entries); for (Iterator<Entry> it = entriesToAdd.iterator();it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entriesToAdd); }
// 剔除那些已经在transactionBundle管理池中的entry对象 private List<Entry> uniqueEntries(List<Entry> entries) { // return entries; //如果出现错误,可以直接return。 // 新生方法的好处就是代码隔离,可以快速定位是修改引入的问题还是原始代码本身就有的bug List<Entry> result = new LinkedList<Entry>(); for (Iterator<Entry> it = entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); if (!transactionBundle.getListManager().hasEntry(entry)) { result.add(entry); } } return result; }
当然也可以引入外覆方法的手法。
外覆方法的第一步总是重命名原有方法和引入外覆方法,外覆方法名就是原有方法名。这一步基本不会错。
//rename "postEntries(List<Entry> entries)" aspostEntriesDirectly private voidpostEntriesDirectly(List<Entry> entries) { for (Iterator<Entry> it =entries.iterator(); it.hasNext(); ) { Entry entry = (Entry)it.next(); entry.postDate(); } transactionBundle.getListManager().add(entries); } // new wrapper method use signature "public voidpostEntries(List<Entry> entries)" public voidpostEntries(List<Entry> entries) { postEntriesDirectly(entries); }
下一步,调整外覆方法的实现,这里基本与新生方法相同
// new wrapper method use signature "public voidpostEntries(List<Entry> entries)" public voidpostEntries(List<Entry> entries) { // 先剔除那些已经在transactionBundle管理池中的entry对象 List<Entry>entriesToAdd = uniqueEntries(entries); postEntriesDirectly(entriesToAdd); }
如果习惯了思考使用弱侵入式的修改方式,后面两种方式会自然而然的得到。外覆方法与新生方法的区别是外覆方法保留了原有方法(只是方法名做了修改)。
如果有需要,还可以新生类和外覆类。原理都差不多。
最后啰嗦一下,如果一开始需求是这样描述的:
对entries列表中的Entry对象,首先要检查是否已经在管理池中。只有不存在时才执行postDate()操作,并把它添加到管理池中。
这样描述后,要想到后面两种方法就会更自然一些。
所以说需求描述是很关键。但是没人会为我们做这个,一切只能靠自己。一切从需求分析开始。