读《修改软件的艺术》

分类: 阅读

最近工作中又面临特别令人纠结痛苦的事儿——明明是自己新设计的程序架构,好多优势都吹出去了,现在又觉得有很多设计失误,想要大幅度重构,以至于像是重写了。

我倒也理解体谅自己,谁也不能把事情一次做对,很多经验也都是在做的过程中积累起来的。往往是做过一次,第二次才能把事情做得稳健,而后也不可避免地有第三次第四次的不断改进。

只是,第一次的水已经泼出去了,第二次再做总要受到兼容性的限制,束手束脚,因此想来图书馆找点安慰。

翻到这本书,一上来就看到“遗留代码危机”;再看到目录,也有大量有关“遗留代码”的方法论介绍。于是直接在书架旁边就看起来了,直到闭馆……


这算是本哲学书,又或者有点像鸡汤,但也有不少方法论——我其实不必写太多感想,毕竟你要有兴趣,照着做就好嘛!
不过呢,我结合自己的经历聊一聊这本书,想必更能引起读者的兴趣,也让鸡汤展现得更立体一些。

下面分享一些摘录和感想,也算做个总结。

为什么瀑布模型不管用

瀑布模型想必大家都知道,一个个开发阶段叠在一起,要求开发过程具有完整详细的规划。

作者举了一个形象的现实类比:盖房子的时候,都会希望所需的材料尽在手边准备就绪,照着图纸盖就好。
瀑布模型也是类似,将产品拆分成不同功能来开发,最终再打包成成品,是一种符合直觉的先部分后整体的逻辑。

然而作者说,在虚拟世界中,这不奏效。

瀑布模型意味着长发布周期,也就是说几个月后才能看到代码真正执行——即便是执行,往往也只是测试环境,与最终实际应用的生产环境可能又大相径庭。这就是一个巨大的风险。

很多人以为,瀑布模型的主要问题是流程繁杂,周期长,因此效率低下。作者却不这么想。

盖好的房子,就在那里了,也不会再加盖额外的房间;但做好的软件,总会被频繁的添加新功能,被用于不同的环境。

效率低下不是主要问题,难以改变才是。

在增量构建的实践中,开发者往往会采用一些扩展机制,以便后期变更;但瀑布模型中,开发者几乎不考虑这件事情,只求按照“图纸”实现完成。

作者又用食谱和配方类比。

厨师根据食谱制作菜肴,可以有自己的发挥,调整材料添加的量和种类,制作出不同风味。

而面包师烘焙面包,酿酒师酿酒,都要严格按照配方,可能稍微改变配料的比例,就会直接导致产品的失败。

软件开发应遵照食谱,而非配方。因为,能够改变对于软件而言是无比重要的事情。就像我在关于重构那本书的读后感中提到的,“为有源头活水来”才是让一款软件产品的开发长久不衰的根本。

专家知道些什么

作者举了一个医学界的例子:医学界曾嘲笑关于微生物导致疾病的理论,不认可外科医生术前洗手这件事会影响病人生死。当时细菌学并未出现。

就像当时大多数医学界人士一样,软件开发领域也充满了“外行人”。

“代码的质量并不重要,重要的是软件正常工作”——这是常见的错误认知。

软件被使用,就会被修改。没有质量保证,就难以修改。为了降低软件支持成本,就必须关注软件的构建过程。

一般来说,专家的认知上下文与一般人不同,知道重点在哪里。我们应当多听听专家的意见。就像我们熟知的一个故事:专家修理一个机器,只在机器上画了一道线,说了操作方法,便开价一万美元。他说:“用粉笔画一条线1美元,但知道在哪里画线9999美元。”

软件开发领域的专家,优秀的开发者往往是整洁的程序员。他们速度快并不意味着他们粗心大意,事实正相反——快速的程序员特别注意让代码保持容易维护的状态,会频繁调整方法的位置,重命名变量和方法,及时删除无用代码……

他们不会不顾代码质量而加快速度,反而因为保持了代码的高质量才能保持快速!

产品负责人

产品需要负责人,对整个项目全权负责。他们应当和客户接触最频繁,对产品理解最深刻。

作者说,大多数由委员会设计出来的东西都行不通。这让我不禁想到前几天看到知乎上轮子哥吐槽C#编程语言,自从Anders大神不管了,C#委员会的审美走向也越发奇怪了。不过这毕竟还只是面向开发者的编程语言,若是面向外部客户的产品,“两耳不闻窗外事”的委员会“闭门造车”肯定是不行的。

产品负责人未必是技术人员,但需要引导推动开发流程。

开发者往往会想到一些一般人想不到的问题,因为编写软件需要考虑很多条件和潜在问题,而需求制定者往往只考虑了最常见的用例(快乐路径)。开发者的这些问题,需要有人来回答——回答这些问题也是产品负责人的责任。

开发者搞错事情,往往不是搞错代码,而是搞错代码应该做的事情。所以由产品负责人来对一些问题拍板定论相当重要,避免产品开发不断走偏。

用户故事

既然是用户故事,就应该讲用户需要什么,而不是开发者应该怎么做。

同时,讲清楚功能为什么需要,是做给谁,也会大大帮助开发者理解功能的使用场景,以便做出更好的开发设计。

用户故事还要有验收标准,明确告知开发者做到什么程度算是收工。毕竟开发者很容易做出过度设计,耽误宝贵的开发时间。

我们肯定希望确定性,因此希望为工作单元做出时间规划。而小的工作单元,更容易做出时间规划。

“小”到可度量即可。

更小的谎言

现实中总有突发情况,我们的“规划”往往都只是“鬼话”——自我欺骗的谎言——说三天做好最终做了五天是常有的事儿。

不过,谎言未必总是贬义的。如果谎言足够小,面对真相到来,我们就不必惊慌失措,因为我们可以快速调整——把三天调整成五天,并不会造成多大的影响。

这是敏捷的本质:设立小目标以支持尽早快速调整。

分而治之与未知

我们需要把问题分解。简单说,我们要把问题分解成已知的和未知的。

已知的问题我们能做出相对确定的规划,对于未知的问题,我们则可以采用两种方式来对待:

  1. 将未知变为已知。这个不用多说。
  2. 封装未知,把未知的东西藏起来,留待日后解决。
    就像是“搁置争议,共同开发”的南海政策,把困难问题留给更有智慧的后代解决。对于软件开发中的未知问题,我们也完全可以暂时提供一个已知但“丑陋”的实现,封装到API内部,足以支持其他功能正常运转即可。日后也许团队里进来了一位才子,可以替我们修改这个API的实现了。

待办列表

我们需要MMF(Minimal Marketable Feature set,最小可市场化功能集)来支持增量式开发,因此必然会有做事的先后,也就需要一个待办列表。

对于待办列表,我们要讨论的并非事项的优先级,而是完成他们的顺序

产品负责人应该是唯一拍板决定下一个构建什么的人。

优先级这个属性,其实很不显然。有时候看起来虽然是最重要的功能,但可能过于复杂,不如等到次要的功能先完成再做,也许就事半功倍了。

当然,对于待办列表的计划,有一点很重要——不要打断。已经在迭代的功能,就继续坚持做下去。新想法都应该安排到下次迭代,或者未来的某次迭代中去。

完成

什么是完成?作者也给出了几种不同“完成”的定义:

一般的“完成”,就像是瀑布模型中,编写完一个功能,可以在开发机器上执行。

“完整完成”,指代码不仅在开发机器上正常执行,还应经过了集成。

“完美完成”,则是指代码经过集成,而且清晰健壮,易于阅读和维护。

这样一种定义也就告诉我们,不要轻易说自己“做完了”。我们所谓的“做完了”,往往都只是一般的“完成”,“完整完成”都很少(毕竟多少Demo都是在开发机器上完成的呀!)。

只有代码到达了并保持住“完美完成”的境地,产品的开发才能说是立于了可持续发展的不败之地。

持续部署

作者说的绝大部分,我想大家都应该熟知了。这里只强调一点,我们应当将配置文件、数据库模型、测试代码和脚本、第三方库、安装脚本、文档、设计图例等等等等全部进行版本管理。

换句话说,持续部署就应该能够通过版本管理中的一切从零部署好一整套产品系统。

结对编程

“结对编程”,以前仅有耳闻,一直觉得像是闹着玩一样,想想就很尴尬——难不成是传说中的“面向‘对象’编程”!

看过这本书的介绍,才想象到“结对编程”的魅力,逐渐开始接纳认可,甚至忍不住想去实践一下。

结对编程,可以让资深开发者直接指导经验欠缺的开发者,可以让大家互相学习。
同时,有人关注的情况下,开发者不容易偷懒,也不容易写出糟糕的代码,大家也会更容易统一编码风格。
结对编程能够形成代码的集体所有权,有助于代码的风格统一,便于阅读维护。

另外,作者也提到,两个人在一起交流合作的时候,一般不会有人来打扰;但一个人默默无闻写代码的时候,时常就会有人来打扰一下了。这也算是一个不小的优势。

结对编程的形式一般是有一位“驾驶员”,负责敲键盘实际写代码;还有一位“领航员”,给予及时的评价,包括认可赞赏和指正批评。

结对的搭配可以随机生成,毕竟熟人之间容易尴尬或者懈怠,结对效果不佳。

其他解决难题的方式

  • 穿刺:两个或两个以上的开发者一起干一件事
  • 群战:整个小组做同一件事,但分别进行。(这就有点像《流浪地球》里面的饱和式救援)
  • 围攻:整个小组一起分解着做一件事。

测试驱动开发(TDD,Test-Driven Development)

简单说,先写测试,后写实现。

有人会问,没有实现,测试怎么写?

可以先定义接口,或者类的成员,但让具体实现均为空。接着按照正确的工作流编写测试,调用这些虚假的接口。

此时,测试肯定是失败的。开发者接下来就应当通过实现接口来完成整个测试驱动开发。

这里的哲学就是:先想做什么(也就是怎么被应用,测试就是在应用我们的接口),再想怎么做。

当然有时候先写好的测试,很难做出使其正确执行的实现。这本书在这里也提到:“第二次做好”。
我们不可避免地需要反复修改测试用例和实现,作者说就像二者在对话一样。程序员有时就是要做到人格分裂。

摘录一段书中原话:

在编写测试的时候其实是在说:“你好代码,你能做到这些吗?”

代码当然不能,所以测试失败。

得到了否定回答,你说:“好吧,让我来告诉你怎么做。”

之后你编写完实现,然后说:“你好代码,现在你能做到吗?”

然后如此往复。

重构技巧

本书中提到了几个重构技巧,这里选我印象深刻的两种:

  1. 图钉测试。这是一种可能覆盖成百上千行代码的粗粒度测试,可以保证端到端行为的正确性,也就能够帮助我们在重构的过程中检查是否产生对结果的影响。
  2. 系统扼杀。指用自己的服务包装原有的服务,不断替代它,直到最终扼杀。

最近在工作中,我们就应用了这两项技巧:

首先是在维护古老的代码时,想做一些性能优化的重构。但这份代码被用于很多地方,如果有功能结果的变化,会为用户带来巨大的维护成本(例如大量的数据更新等)。因此,为了避免有结果的改变,我们直接找了一份巨大的数据作为输入,跑古老的代码,并与重构后的代码跑出的结果相对比,以确定重构没有造成任何功能效果的影响。

另外,系统扼杀也常常我在工作中遇到。我们需要维护古老的代码,但也同时在开发现代化的新产品,可是就像前面提到的,兼容性往往是很多客户重视的。因此我们希望渐进式的替换,把古老的代码包了一层与现代化接口相似的接口,希望将来能够逐渐被取代。