引子

笔者自从2009年1月份参加编程开发工作以来已经进入了第八个年头;工作与生活时常会遇到问题,一些是有“标准答案”的技术问题,而更多的是非技术性的问题,例如习惯、方法论、人的问题等等。软件系统推倒重来经常出现于软件技术团队中,往往是团队对一个已经“失控”的项目采取的 最终 手段,然而我们中的绝大多数开发者,尤其是 领队 似乎并没有认识到 问题的本质 。笔者最近在一些前人的经验与其他开发者的交流下,对此领悟到些许问题的核心。

货物崇拜式编程与货物崇拜式软件工程

货物崇拜(英文:Cargo Cults,又译货物运动)是一种宗教形式,特别出现于一些与世隔绝的原住民中。当货物崇拜者看见外来的先进科技物品,便会将之当作神祇般崇拜。

塔纳岛上的十字架 最为知名的货物崇拜,是瓦努阿图塔纳岛的“约翰弗鲁姆教”(John Frum Movement)。第二次世界大战太平洋战争时,美军于塔纳岛建立一临时基地。当时岛上的原住民看见美军于“大铁船”(军舰)内出来,皆觉得十分惊讶。此外,他们也看到,有一些“大铁鸟”(军用飞机)运送穿着美军军服的人及许多物资。这些原住民看见这种情况均感到很惊讶,并觉得这些“大铁船”及“大铁鸟”十分厉害。加上美军也提供部分物资给原住民,而这些物资对原住民来说十分有用,结果令这些原住民将美军当作神。 第二次世界大战结束后,美军离开塔纳岛,只留下一些美军军服及一些货物。塔纳岛原住民便认为这些货物具有神奇力量,又相信“神”(美军)他日会回来并带来更多货物,使他们展开一个幸福新时代。但是美军当然再也没有回来塔纳岛,因此这些原住民便自己发展出一套敬拜仪式,崇拜美军军服及货物;表现形式是原住民会穿着美军军服、升起美国国旗,图腾则是木刻的飞机。

——摘自维基百科1

以笔者个人的经历来看,软件工程的程序员异常喜欢推倒重来,这也许是思考方式上的一个共性现象。在解决业务需求的时候,往往采取“ 解问题, 做优化”的思路;从整体策略上来说,笔者也是同意的;然而问题的重点在于 “解问题”和“做优化”这两步都是必须贯彻到底的重要组成部分 。很多情况下在做好解决问题的情况下,多数忘记重构优化,这几乎总是导致这样的结果:伴随着项目运行时间的推移,熟悉原本设计思路的人员调动,新需求的小修小改,系统运行效率开始变慢,界面卡顿,时常出现影响业务的性能瓶颈,错误不易排查调试,这都是我们开发人员日常的切身体会。多数这个时候团队里的所有人技术都会同意这样的方案: 我们推倒重来,重新做一套吧。 相信读者们很可能也有类似的经历。

在着重强调 开发速度 ,周期迭代的web类应用上,由于使用的技术多数具备灵活性高的特点,使得“重做”这一行为的成本,在 表面上看似乎异常低廉 ,当问题出现时总是趋于使用简单粗暴地加服务器、重做系统来“解决”问题。

笔者就经历过这样一次“推倒重来”(没有参与策略制定与实际开发,但参与了性能调优),业务项目是手机应用的服务端,原本使用PHP;业务模式上的特点是定时会有类似秒杀时的访问高峰。在系统出现明显性能瓶颈时,任由团队技术人员盲目地更换框架,从一种纯PHP的框架更换到了基于C扩展的PHP框架,收效甚微。后来在领队的指导下对某个有明显访问高峰即秒杀环节进行针对性优化(安排任务队列异步由后台常驻进程处理)才有明显改善。当业务访问量再次增长导致瓶颈再现后竟认为是PHP在服务端效率不够优秀(占用了大量CPU与内存资源但吞吐量并没有提高),并采取了将开发语言更换为Python的决定。其实该领队是有意识知道真正的瓶颈是在数据库这一端的,前后数据库经历了多次硬件性能升级直到几乎无法对数据库再进行升级时只能放弃拖延战术,转而真正的进入数据库拆分来面对问题的核心。

In the South Seas there is a cargo cult of people. During the war they saw airplanes with lots of good materials, and they want the same thing to happen now. So they've arranged to make things like runways, to put fires along the sides of the runways, to make a wooden hut for a man to sit in, with two wooden pieces on his head for headphones and bars of bamboo sticking out like antennas—he's the controller—and they wait for the airplanes to land. They're doing everything right. The form is perfect. It looks exactly the way it looked before. But it doesn't work. No airplanes land. So I call these things cargo cult science, because they follow all the apparent precepts and forms of scientific investigation, but they're missing something essential, because the planes don't land. — Richard Feynman

对于偏好推倒重来的做法,笔者是这样认为的:与越来越飞速发展的信息化社会一样,因 现象与结果的直接联系异常“明朗化” ,使得大部分不市场深层次思考问题的人产生了一种 错觉 ,即“只要我也学着这么做,也能做成”。“既然连某某专业人士也认为PHP有那么多缺点”,而自己在使用经验中发现Python没有这些缺点,再加上那么一点点偏爱作为催化剂,此时对于“推倒重来”这项决定的心理暗示就被无限放大,强烈得作用于有决定权的人脑中。诚然笔者也同意,在专业化程度不那么高的问题上, 一般 依葫芦画瓢,照做的方式是可以行得通的。但毕竟这只是 认识问题的初级阶段 ;我们往往会学着那些“成功”、“有成效”的做法来解决问题,并以经验主义的观点认为 这就是正确的解决方法 。笔者并不否认歪打正着的可能性,然而仅仅看到问题的表象或者说认识问题的初级阶段 对解决复杂,深层次问题往往是会产生误导的

这就好比《代码大全》的作者Steve McConnell在其文章2中提到的,“货物崇拜”,只看到了“大铁船”,“大铁鸟”上天神下凡,带来诸多先进实惠的物品便以为只需要学者机场塔台的做法,“修建跑道”,“竖起塔台”就可以让“大铁鸟”再次降临。

不同层次的问题,需要不同层次的思考。深层次的问题,就需要经过深层次的思考,得到更深层次的结论。

不要轻易推倒重来

乱象,印迹博客的作者余晟在其文《“推倒重来”的讲究》3中提到

众所周知,软件开发的难点之一就是控制复杂度。但是在不同的领域,复杂度有不同的表现。对于纯互联网业务,或者IT基础架构来说,其复杂度在于软件本身,架构的制定、类库的选择、编码的质量等等。对于其它IT系统——尤其是公司迅速成长,业务不断复杂化的IT系统——而言,其复杂度并不在于软件本身,安全、性能、负载的问题都套用现成的IT解决方案,真正的复杂度来自系统承载的业务本身,比如最简单的:系统里有哪些单据,各种单据承载什么信息,用在什么场景,这些单据是怎样流转的,各种单据存在怎样的约束关系,出现异常情况应当如何处理才能保证业务数据的一致性……这些问题没有准确而稳定的答案,IT再怎样努力也是白搭。

我们说软件工程的管理 不仅仅是对软件代码的管理 ,而是对开发过程中 每一个环节的行动作出指导,对产出进行保障

软件系统推倒重来的初衷是,软件系统有不符合预期的表现,或者说这些现象是随着时间慢慢推移出来的,一些是 因为随着规模的增大逐渐失去控制 ,另一些是外部环境地变化, 软件系统已不能满足目前变化后的用户期望与需求 。单纯地推倒重来,对解决这些问题 几乎是没有帮助的 ,或者说 即使解决了问题的现象,也没有帮助我们认识问题的核心在哪里,难以对未来提出建设性的指导

大多数推倒重来时只考虑到重新做带来的好处,却没有考虑到 重新做时也同时带来了新的问题

余晟在其文《技术领导需要比下属更懂技术吗?》4中提到

在我刚工作的时候,业界使用的Java(当时不少人还用的J2SE这种“专业”的说法)的版本是1.4.2,而Java 5.0的版本已经推出了,并且Sun做了大量的工作,宣传Java 5.0的各种好处。我作为充满好奇的职场新人,当然也鹦鹉学舌地“明白”了不少,比如范型,比如改进的for循环等等。相比之下,实际项目中老版本代码太多的“陋习”已经让我跃跃欲试,要大修大改一把了。不过,要做到这一点,我首先需要获得项目经历的许可。于是我仔细准备了几天,凑齐了一些自认为有说服力的资料,然后跑去跟项目经理建议,我们应该升级到5.0版本。

我永远都记得他当时的反应:先是一愣,然后说,“但是我们已经很熟悉1.4.2了呀,而且这个系统长期以来都是跑在1.4.2上面的,很稳定。你建议的这些特性,并没有太多实际的好处。” 听了这话我想,他虽然做过不少项目,但脑子已经不够更新了,一直停留在1.4.2的时代了,这就是他否定我的建议的根本原因。“不过,如果你有兴趣,也可以先做一个仔细的调研,然后模拟环境测试一下,看看5.0到底能不能跑。” 既然他最后给我留了个希望的口子,我还是奋力去准备争取,耐着性子尽可能详细地做了试验。果然,我发现直接升级到5.0有问题,有个依赖的第三方库会产生兼容问题。当然,最终升级方案还是通过了,系统也有惊无险地升级成功。但我回头想想,却不得不佩服项目经理的保守:如果冒失升级上去,估计生产系统就不转了。更让我困惑的是,虽然他熟悉的版本是1.4.2,但他似乎不太关心5.0到底有哪些进步,也没怎么花时间去了解这些进步。

再回到笔者的例子中,在更换开发语言的期间出现了一种很有趣的现象,领队对PHP是带有一定偏见的,并且对于Python有偏爱,而团队中的技术人员也对学习使用“新的技术”表现出巨大的热情,这就出现了一幕很诡异的情景:从上到下一拍即合,说服自己 更换开发语言对解决系统吞吐量瓶颈有帮助 (笔者真心怀疑到底有几个人是真心这么认为的)。如今随着时间的推移系统瓶颈依旧在,还是需要 脚踏实地地从性能分析数据出发找出瓶颈环节一一加以解决 。但木已成舟,一次决定性的影响引入新的问题, 这些PHP工程师如今看上去似乎成为了Python工程师,殊不知Python固有特点和PHP比起来还真说不准谁高谁低,但这些工程师离开了熟悉的PHP环境, 如今甚至需要面对基础的Python问题

要知道,选择技术工具,即是选择了其优点,特性,好的方面的 同时,选择了其不那么好的方方面面

笔者在这里想强调的是, 管理者、决策者在面对问题时扮演极其重要的角色 , 领队是解决问题核心的关键角色 却没有传达问题的核心在哪里,应该如何正面解决 ,却似乎给予团队中一个错误的信号:即遇到问题不分析原因,转而更换技术工具能够解决问题。

人是非常易于受到所谓“人格魅力”,“榜样作用”影响的。至于是好是坏,只能留给时间来检验。

认识问题、理清问题

需求是依据

技术提高生产力,技术是用来解决问题的,我们在做决策时不仅仅需要考虑方案是什么。从问题的本质来说,就是认识需求是什么。没有明确的需求,几乎无法定义清晰的问题范围,那么软件系统的功能职责范围也就无边无际了,这个结果相信是熟悉软件工程的各位读者不言自明的最可怕情况。软件系统应以人、以业务、以解决问题为本。

对于软件需求分析,有各大资讯公司的不同理论。笔者以自己的做法为例: 把业务需求写下来 ,如果需求写下来后其中个语句无法清晰地联想到系统设计的概貌,那么就需要更深入具体的描述这个需求。

既然贸然推倒重来不是一个好做法,笔者建议不妨看看一个好的做法是什么样子的,笔者在接收维护一个老软件系统,重构一个软件系统,或者新设计一个软件系统时,会填写这样一个模板, 并将至放至团队或技术部门的wiki页上

  • 解决哪些问题

  • 这些问题是否已经有现成的流程,如没有则需要总结并文本化

  • 有哪些人(角色)参与到这个需求中

  • 会产生哪些数据

  • 不同需求间是否会有关系

等等,大多数时候将需求文本化后就会发现其中不明确,概念不清晰的地方,此时就需要进一步深入明确。明确到何种程度,依据各位自己的习惯,笔者的经验是明确到相对独立的完整功能项目即可,一些明显违反直觉或者难以用一两句话理清的 设计 ,也需要写下来。明确文本化需求的意义在于,它是 定义系统功能范围,是从结构设计一直到测试迭代,乃至重构的唯一依据 ,并且当功能不满足时很容易地判明到底是功能缺陷,还是需求发生了变化。

数据是核心

Linux内核的原作者李纳斯·托瓦兹在谈论到git跨平台移植的问题时5,曾有过如下的观点

I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.

笔者表示赞同前半句,即 好的程序员注重数据的结构与关系 。代码的结构当然也很重要。

为什么说数据重要,在软件系统中数据 往往与业务绑定紧密,不易轻易改变 。有发生改变一般都是由于两个原因,一是不满足业务需求的变化更改;二是数据的设计已不再符合整体的设计,需要重构。

数据本身也是需要进行分类的,对于软件系统来说一般在持久化存储数据中需要至少分类出重要的核心类数据,理清它们的关系对解决业务问题的理解也是巨大的帮助,并在系统进行改进时能分清解决问题的轻重缓急程度。除此之外,业务流程的日志数据,统计数据等等也都是重要的整理对象。

就笔者的经验来说,在填写完前一节需求内容后,便来整理数据。这时 软件工程上的基础就发挥重要功能了

  • 实体-关系图

  • 数据流图

  • 状态转换图

都是系统分析的必备技能与基础。

(题外话:笔者在诸多技术博客中会看到对中国的计算机教育提出异议,说学校里教的都是过时的东西,甚至揶揄成“计算机考古学”。笔者则认为,至少在软件工程上,学到的知识与技巧大部分在实际工作中是 有机会应用,并行之有效 的,关键看什么时机,如何应用。)

解决问题

注重合理设计与维护性

对于满足业务需求的软件系统来说,运行时间都是长期的(至少长达一至两年以上),那么设计合理与否事关重大。除了依据大原则,笔者认为还应该不断总结日常遇到的问题,深层思考后将其反馈到设计上,不断地进行重构。

对于系统的设计原则,笔者曾在《开发系统时的四个要点》6一文中有所初探。

当软件系统出现瓶颈,无法满足需求时,笔者认为依旧切忌盲目推倒重来。应当 脚踏实地 收集分析性能数据,找出性能瓶颈的环节,评估其严重程度,根据实际情况进行针对性优化或容量扩展,对于过时的结构、模块、组件作出合适的调整与重构。表面上看似乎在逃避问题没有提出整体性解决方案;但笔者认为恰恰相反,在这个过程中收集到的数据、经验再经过深层次的分析总结思考,以及不断的重新规划。

对于外来的解决方案,新的技术也需要持续地进行严谨地评估,摸透其优点,缺点是否适合应用场景。

才能为 最终解决方案 (也许也是一些情况下避免不了的) “推倒重来”,提供丰富、有效、明确的第一手依据。

后话

上文提到这些更换开发语言的团队中技术人员目前如何?他们既没有认识到系统吞吐量瓶颈问题的核心在于共用一份数据库实例,也没有认识到对数据库拆分需要更全面、长远的设计;同时对于领队吐槽PHP的问题是否真的理解了,或者使用Python时还按照PHP的思路实现使用都存在不同程度的理解偏差。很多情况下在推倒重来的过程中, 既没有整理需求,也没有重新审视结构、数据设计是否合理 ,就笔者看到的情况,多数时候都是领队催着上线,所以只能 勉为其难直接把PHP代码翻译成Python代码吧

本文笔者想表达的核心思想

  • 解决问题 需要认识问题,认清需求,并对其中的核心问题与本质解决方法有理性的认知

  • 对于软件系统的技术问题,小到升级一个第三方库,大到对系统改造甚至“大刀阔斧地推倒重来”,都不可掉以轻心。

  • “做事在人,成事在天”的本意是指,做事的人已经竭尽全力,把自己不确定的东西交给运气。 但是反过来说 ,当做事的人有实力的时候,何必要去碰运气,饯行机会主义? 这是笔者近期的口头禅:“ 有实力的时候,为何要碰运气?

话又说回来,如今瓦努阿图已今非昔比,虽然物质条件依旧落后。不过据笔者认识的一位远在瓦努阿图当地支援建设的朋友说,在中国的支援下瓦努阿图也在努力建设基础设施谋求更好的发展。

\_\_END\_\_