流水沉微

如何构建好软件

译者序

在读到此文时感觉非常不同于其他类似文章,本文的立意很高,从战略角度阐述了跟人月神话类似的观点:软件开发最重要的是保持专注和精简、控制复杂度和招募最优秀的工程师,单纯加人和加时间都不能解决问题

让本文更具有说服力的是,作者李鸿毅曾为谷歌工作,并带领研发团队为新加坡的公共服务部门做出了广受好评的多款应用。此外,李鸿毅还是著名的C++程序员——新加坡总理李显龙——的儿子

原文链接:How to Build Good Software

软件的特点导致了我们很难用传统的管理方法来构建软件;我们需要用一种不同的、更加有探索性和迭代的方法来提高开发效率

作者 李鸿毅
发表日期 2019.08.08
分类 创新、软件开发

一、为什么优秀的人也会做出烂软件

烂软件是世界上少有的你不能用钱来解决的问题之一。市值数十亿美元的航空公司的航班搜索App常常是一帮学生的课程设计捣鼓出来的;在横跨世界的出租车公司在共享租赁服务的竞争面前依然有着极其难用的预订App;令人痛苦的企业IT系统往往耗资巨大还需要很多年的时间来构建。不管烂软件的成因是什么,看起来都不像只是缺少资金

令人惊讶的是,烂软件的根源更多的不是特定的工程层面的选择,而是整个开发项目的管理方式,最烂的软件通常是这样开发出来的:

项目负责人怀着要构建某个特定解决方案的愿望开始了整个项目,但却从头到尾都没有明确得定义过他们到底要解决什么样的问题。他们从需求方那里搜集来一个长长的需求清单,然后清单被原封不动得提交给了一个外包开发团队,并要求外包从头开始开发这个高度碎片化的软件。一旦需求得到满足,所有人都开始庆祝系统运转、该项目完成

烂软件的根源更多的不是特定的工程层面的选择,而是整个开发项目的管理方式

然而,尽管该系统满足来预先订好的规格要求,但是真正将软件交付到实际用户手中时经常会发现非常严重的问题。系统用起来很卡,交互令人迷惑,系统中充满了各式各样的小bug导致使用起来就是对耐心的消磨。不幸的是,在这时候外包团队已经不在了,也没有预留出足够的资源来进行必要的修改。当一个新项目上线数年之后,所有关于这些问题的知识都随风飘散,上面的循环就开始了

在不同项目之间,合适的编程语言、系统架构、接口设计都差别极大,但是有些(共通的)特征会导致传统管理实践一再得失败,却使很小的创业团队只依靠微薄的预算获得巨大的成功:

  • 对好软件的重用非常容易;这让你可以快速构建好软件
  • 软件所受到的限制不在于构建软件时投入的资源的多寡,而在于在软件彻底垮掉前该软件可以有多复杂
  • 软件所蕴含的主要价值不是为了构建软件而编写的代码,而是构建软件的开发者所积累下的知识

即便理解了上述特征也不能完全保证就一定能得到好结果,但是它们确实在解释为什么如此多的软件项目都最终产出了一堆垃圾时很有帮助。更进一步,它们会引出一些可以戏剧性得提高成功概率的核心法则:

  • 开始的时候尽可能简单
  • 明确要解决的问题、然后迭代,小步快跑
  • 雇佣你找的到的最好的工程师

尽管还有很多更细微的因素需要考虑,以上法则会成为你开始构建一个好软件的必要基石

二、对好软件的重用可以让你快速构建好软件

软件非常容易被复制。在原理层面,一行行的代码可以直接被复制粘贴到另外一台电脑。在更广的范围来看,互联网中也充斥着各种各样的教程,教你如何使用早已编写好、在网络上唾手可得的软件模块构建不同的系统。现代软件已经几乎不可能每一行代码都从头写起来,即使是最创新的应用也是使用现有的软件通过组合和修改来得到的结果

最大的可用代码模块来自开源社区。开源软件是指那些代码被自由得发布、可供任何人浏览和使用的软件。许多最大的开源社区贡献者是那些巨大的科技公司。如果你想像Facebook一样使用这个星球上最先进的可伸缩数据库,只需要下载Facebook在2008年开源的Cassandra的代码;如果你想自己尝试一下Google最先进的机器学习,下载它们2015年公开的TensorFlow系统。对开源代码的使用不仅使你的应用开发速度更快,它还使你能接触到比你自己能独自开发的任何软件都要复杂得多的技术。对那些最流行的开源代码来说,它们甚至更加安全,因为有非常多的人在关注着它、为它修复各种漏洞。这也是数字技术有如此快的变化的原因:哪怕是刚入门的工程师也能在行业所能提供的最先进的工具的基础上进行创造

云服务的出现让重用能力得到了更大的发展,仅仅只需要支付一点点订阅费用就可以得到一个特定的系统。需要一个简单的网站?只需要在类似于Squarespace或者Wix之类的建站服务上稍微点击即可;需要一个数据库?在Amazon Web Services或者Microsoft Azure订阅一个虚拟的服务即可。云服务让开发者享受到专业分工的好处:服务供应商会处理掉所有的创建、维护工作,并保证持续开发可靠、高品质的基础服务提供给所有订阅者。这可以让开发者不再浪费时间去解决这些问题,转而专注于交付真正有价值的内容

如果你所有的时间都花在重头构建已经被其他人开发过的软件上的话,你不可能取得任何科技进步。软件工程是用来构建自动化系统的,这其中最首要的之一就是让所有例行公事都自动化。最关键的就是要找到合适的系统来重用,让它们能处理你的独特需求,并在过程中处理好新出现的问题

软件工程是用来构建自动化系统的,这其中最首要的之一就是让所有例行公事都自动化

三、软件受限于复杂性

一个软件有多有用通常是受限于它的复杂性而不是在构建它的过程中投入了多少资源

一个有很多功能却依然被用户讨厌的IT系统通常是因为它太复杂了,相反,评分很高的手机App一般都会因为简洁和符合直觉而受到称赞。学习使用软件是很困难的,不合常识的是,软件的新功能实际上对用户来说是更糟糕了,因为它们引入的复杂性开始让软件变得难用。举个例子,在作为Apple的媒体聚合服务工作了近20年后,iTunes今年被拆分成了3个不同的App:music、podcasts和TV shows,因为它里面所承载的功能已经多到一个App难以应付。从可用性的角度来看,软件的极限不在于有多少功能可以实现,而在于哪些功能能被放入一个简单、符合直觉的界面

哪怕忽略掉可用性,工程进步也会慢慢因为一个项目过于复杂而陷入停滞。每一行新引入的代码都可能会跟其他任何一行代码有关联,一个应用的代码库越大,为了添加一个新功能所引入的bug就会越多。到最后,新bug带来的负面影响会吞噬掉新功能带来的价值,我们称之为「技术债」,技术债是专业软件开发中的主要挑战之一,这就可以解释为什么如此多的IT系统会存在很多常年没有修复的已知问题。引入更多的工程师只会增加更多的混乱,他们越快开始工作,代码库就越快因为本身的笨重而崩溃

构建好软件需要另一种循环和减少复杂性

在这种情况下,前进的唯一方法是退回一步,重新分析和简化代码库。系统架构需要重新设计,限制意料之外的交互,非必要的功能可以被移除,哪怕它们已经构建完成,也需要引入自动化工具来检查bug和分析写得很糟糕的代码。比尔盖茨曾经曰过:「通过代码的行数来衡量编程工作的好坏就像是通过重量来衡量飞机建造的好坏」。人类的心智只能处理有限数量的复杂性,因此一个软件系统能有多复杂,取决于这份复杂性预算(开发者的心智)能被多有效得运用

构建好软件需要另一种循环和减少复杂性。只要新功能被开发出来,混乱就会自然而然得开始在系统中积累。当这混乱开始导致问题出现的时候,进一步的开发就需要暂停以腾出时间进行清理。这个两步过程是必要的,因为不存在如此理想的工程:它只是满足你的需求而且恰好能处理好所有你遇到的问题。即便只是简单到如Google的搜索栏一般的简单用户界面也在界面之下包含了一个庞大数量的复杂性,这不可能通过一次迭代就完美得到。这其中最具挑战的是管理这个过程,既要让它足够凌乱以得到有意义的开发进展,又不能让它变得太过复杂而失控

不存在如此理想的工程:它只是满足你的需求而且恰好能处理好所有你遇到的问题

四、软件是探明知识,而不止是写代码

在软件开发过程中,绝大部分想法都是糟糕的,但这不是任何人的错。这是因为可能的想法实在是太多了以至于任何一个想法都可能没办法正常工作,哪怕它是经过仔细筛选和认真思考之后得到的。为了推动开发过程的前进,你需要从一堆糟糕的想法开始,去其糟粕,培育其中最可能有价值的。Apple,视觉设计的典范,会遍历数百个原型才能得到最终的产品。最终的产品可能会看似很简单,但是其背后会有错综复杂的知识来保证它是最合适的那个方案

这其中的知识哪怕在产品被构建出来之后也依然很重要。如果一个新的团队接手了一个不熟悉的软件的代码,那么这个软件很快就会开始变糟。操作系统会升级,业务需求会变化,需要立即修复的安全问题会被发现,处理这些小问题通常比从头开始构建软件更困难,因为需要了解系统架构和设计原则中暗含的那些知识

简要来说,一个不熟悉的开发团队可以使用一些权宜之计来处理问题。随着时间的流逝,新的bug会随着这些权宜之计的临时本质而不断累计。用户界面因为设计语言的不匹配而变得令人迷惑,整体的系统复杂性也增加了。软件不应该被看作是一个静态的产品,而应该被看作是一个开发团队的集体智慧的鲜活表现

软件不应该被看作是一个静态的产品,而应该被看作是一个开发团队的集体智慧的鲜活表现

这就是为什么依靠外包来维护你的核心软件开发是非常困难的。你可以通过外包团队获得一个可以运行的系统和它的代码,但是属于无价之宝的那些构建软件的过程和构建过程中做过的决策、都不在你的组织里了。这也是为什么依靠新的外包团队来「维护」一个系统常常会带来新的问题,哪怕这些系统拥有完善的文档,每当一个新的团队来接手的时候,其中的某些知识都会丢失。经年累月之后,系统就会变成一个经过很多作者一起层层叠叠打满补丁的系统,它会变得越来越难以持续运行,最终,没有人真正知道它是如何运行的

为了让你的软件能长久得良好运行,让你的员工在外部的帮助下不断学习、在你的组织内部保留严谨的软件工程知识是非常重要的

五、好软件开发的三个原则

1. 开始的时候尽可能简单

针对某个特定领域的项目如果打算做成囊括所有功能、大而全的「一站式服务」的话,它多半命中注定在劫难逃。原因显而易见:还有什么比「提供尽可能多的方式方法让用户自己选择合适的」更好的方法来确保你的应用能满足用户的需求吗?毕竟,实体店就是这样处理问题的,比如超市。但不同是,当超市建立起来之后,再多售卖一个种类的物品是相对容易的,但一个拥有两倍数量的功能的App的构建难度和使用难度都远不止两倍

构建好软件需求专注和克制,从最简单的解决方案开始。一个制作精良的简洁App永远不会有「增加一个功能太困难了」的问题,但一个有许多功能的IT系统通常都不可能简化和修复。哪怕是那些「一站式服务」的App比如微信、Grab和Facebook,它们也是从一个特定的功能开始,确保自己的地位之后才开始扩展自己的功能。软件项目很少因为它们太小来失败,倒是常常因为它们太大了失败

软件项目很少因为它们太小了失败,倒是常常因为它们太大了失败

不幸的是,在实践中保持一个项目非常专注是极其困难的,仅仅是从需求方那里搜集需求就足够列出一个巨大的功能列表

控制这种「功能虚胖」的方法之一是使用优先列表。需求当然也要继续搜集,但是每个搜集到的需求都要根据它们属于「必须要有的真实需求」、或是「有价值的附加功能」还是「有的话最好、没有也没关系」进行分类和标记。这样就可以创造一个宽松得多的计划过程,因为所有的功能都不再需要被明确剔除或者明确纳入,需求方也可以心平气和得讨论哪些功能是最重要的,而不用担心某些功能被排除在项目之外。这个方法也可以让在众多功能之间的取舍变得一目了然,需求方在希望增加某个功能的优先级的时候,也不得不考虑将哪个功能的优先级降低。开发团队可以集中注意力于最紧要的问题上,在时间和资源允许的范围内,从上到下得依次解决优先级列表中的问题

我们曾经在我们最成功的App——Form.gov.sg——上实践过类似的流程。该应用最初是一个手动的Outlook宏,我们花了6个小时才处理好我们的第一个用户,但是截止到目前已经处理过大约一百万公开提交了。Data.gov.sg最初从一个开源项目直接复制而来,现在已经成长到能承载300,000访问/月。Parking.sg有一个超过200个待做功能的需求池,虽然我们至今没有抽出时间做,但是它也有了超过110万个用户。这些系统广受好评,不只是因为简洁,也是因为它本身(功能合理)

2. 明确要解决的问题、然后迭代,小步快跑

事实上,现代软件非常复杂、而且变化极快,很少有计划能确保万无一失。就像写一篇好论文,早期鬼画符一般的草稿对最后得到还不错的作品是非常重要的。构建软件也一样,为了构建一个好软件,你需要先构建一个烂软件,然后不断明确要解决的问题,以改进你的项目

最简单的是从跟你的需求方沟通开始。沟通的目标是搞清楚你想要解决的根本问题、同时要避免陷入到一个建立在你先入为主的偏见的解决方案。当我们最初开始Parking.sg项目时,我们的假设是我们需要帮助工作人员,因为工作人员困扰于不得不一直人工计算纸质票据。但是当我们了只花了一个下午跟一个经验丰富的工作人员沟通后,我们发现对熟练的工作人员来说做这些计算是非常简单的。简单的沟通就避免了我们数月的无用功,也让我们能重新聚焦于我们的项目,转而帮助司机。

要谨防伪装成问题描述的官僚主义目标。「司机会对停车券感到迷惑」是一个真实问题,「我们需要构建一个服务司机的App来作为我们的Ministry Family Digitisation Plan(新加坡的一个政府数字化转型计划)的一部分」不是一个真实问题;「用户会因为在政府官网上找到所需信息太困难了而生气」是一个问题,「作为『数字化管理蓝皮书』的一部分,我们需要重写我们的网站以适应新设计的服务标准」不是一个问题。如果我们的根本目标是让市民的生活更美好,那么需要明确哪些东西让他们的生活变糟糕

清晰得陈述问题可以让你在很难从理论上分析的情况下,测试不同方案的可行性。跟一个聊天机器人对话并不比在一个网站上转来转去更容易,而且用户也不想多装一个App,不管这个App能让国家有多安全。对软件来说,显而易见的解决方案经常会在实际投入使用的时候才发现存在致命缺陷。(一开始的)目的不是直接构建最终形态的App,而是首先尽快以尽可能小的代价找到我们真正要解决的问题。用不包含功能的模拟去测试接口和界面设计,用包含一部分功能的模拟去测试不同的功能。用糙快猛搞出来的原型代码可以更快获得反馈,但是任何在这个阶段做出来的东西都要被当作一次性的。这个阶段最想要得到的不是成型的代码,而是对于我们要构建的结果更加深刻和清晰的认识

要谨防伪装成问题描述的官僚主义目标,如果我们的根本目标是让市民的生活更美好,那么需要明确哪些东西让他们的生活变糟糕

当有了对正确解决方案的良好认知,就可以开始构建一个实际产品。你停止探索新的想法,将思路聚焦到处理在具体实现中遇到的问题。在一开始的时候就找很少的测试人员来快速找到需要快速处理的明显bug,当这些问题都妥善处理好之后,你就可以将产品开放给更多的人使用,找到更多更深层次的问题进行处理

大部分人只会反馈一次。如果你从一开始就开放给了一个很大的试用群体,他们都会给你反馈同样的、最明显的问题,但是这对你的产品后续改进方向并没有什么帮助。哪怕是最好的工程师开发的最棒的产品也是从最明显的问题出发,我们的目的是不断优化输出,不断打磨边边角角直到我们得到一个好产品

哪怕经历过这样的迭代过程,刚上线的时候也是一个产品遇到最多问题的时候。一个只有0.1%出现概率的问题在测试期间可能根本就不会被注意到,但是当你有一百万用户的时候,每天未解决的问题背后都是成千上万个愤怒的用户。你还需要在新的手机设备、网络异常、受到安全攻击影响到你的用户之前就修复各种问题。在Parking.sg中,我们构建了一系列附属系统来持续检查主系统中的支付异常、重复泊车和应用崩溃等问题。随着时间的推移,构建一个「免疫系统」可以让你避免在新问题不可避免地出现的时候不知所措

总的来说,(构建好软件的)方法是用这些不同的反馈机制来有效地识别问题。小的反馈机制可以快速和简单地纠正小问题,但是在处理更大范围的问题时会略显乏力。大的反馈机制可以处理大范围问题但却缓慢而昂贵。你也许会想将二者紧密结合起来使用,尽可能多地处理问题,顺便还能有其他机制来搜集意料之外的错误。构建软件不是为了避免失败和犯错,而是战略性地快速试错,以尽快获取到足够你构建好软件所需的信息

3. 雇佣你能找到的最好的工程师

拥有好的软件工程的关键在于拥有优秀的工程师团队。Google、Facebook、Amazon、Netflix和Microsoft都运行着数量多到令人瞠目结舌的IT系统,而众所周知的是,它们为了招聘最优秀的候选人的面试流程极为挑剔,竞争却依然激烈。原因之一是随着公司发展,哪怕是刚毕业的新人的工资也能大幅增长,工资增长的原因不只是因为它们喜欢乐善好施地对员工做慈善

Steve Jobs和Mark Zuckerberg都说过,最优秀的工程师的生产力比平庸的工程师高不止10倍,这不是因为优秀的工程师敲键盘的速度快10倍,而是因为他们作出的好选择能节省10倍的工作量

一个优秀的工程师能更好地掌握他们可以复用的软件,能最小化他们从头构建系统时所需要做的工作;一个优秀的工程师能更好地掌握各种工具,能将工作中的常规内容都自动化。自动化也意味着可以将人力解放出来,去处理各种意料之外的错误,而最优秀的工程师在这个方面做得特别好。优秀的工程师能设计出更加健壮的系统,也能更轻易地理解其他人做的系统。这一点会成倍地体现出价值,因为能让同事在他们优秀的工作成果的基础上,更事半功倍地工作。总的来说,优秀工程师的高效率不仅仅是因为他们自己能更有效地产出更多代码,也在于他们做出的决策能让你不必去做那些你甚至都不知道可以避免的工作

这也意味着一小撮最优秀的工程师经常能比一大群平庸的工程师更快地完成任务。他们善于利用开源代码和业已成熟的云服务,将乏味无聊的任务都交给自动化测试和其他工具,然后专注于工作中那些需要创造性解决问题的方面。他们通过给关键功能划分优先级和剔除不重要的工作来快速测试用户的不同想法。这就是那本不朽名著「人月神话」的中心论点:通常,增加更多的工程师不会让项目进展更快,而只会让项目变得更大

构建软件不是为了避免失败和犯错,而是战略性地快速试错,以尽快获取到足够你构建好软件所需的信息

相对一大群的平庸工程师,一小撮的优秀工程师制造的bug和安全问题也更少。就像写一篇散文一样,作者越多,最终文章里需要协调的的代码风格、假设和怪癖也就越多,从而暴露出更大的潜在问题。相比之下,一个由更少的优秀工程师组成的团队所构建的系统将会更简洁、更连贯,也更容易被其创建者所理解。没有简洁性就没有安全性,而大规模协作很难得到简洁的结果

工程上的协作越多,工程师就需要越优秀。工程师代码中的问题不仅影响他的工作,也影响他同事的工作。在大型项目中,由于错误和糟糕的设计选择像滚雪球一样带来大量问题,糟糕的工程师们会互相挖很多坑。大型项目需要建立在坚实可靠的代码模块上,这些代码模块要求在高效设计的基础上,有非常明确的假设。你的工程师团队越优秀,你的系统在因为本身的笨重而倒塌之前就能越大。这就是为什么最成功的科技公司尽管规模庞大,却坚持聘用最优秀的人才。系统复杂性的硬性限制不是项目的工作量,而是质量

结论

好软件开发始于对想要解决的问题有清晰的理解。这让我们可以测试许多可能的解决方案,并聚焦于一个好方法。可以通过复用开源代码和成熟的云服务、使用已经建立好的软件系统和其他复杂的新技术来加快我们的开发速度。开发周期在探索和整合之间交替进行,在快速验证新想法时可以稍微混乱,然后将结果进行精简以保持复杂性的可控。随着项目推进,引入更多的人进行测试,逐渐消除其中越来越不常见的问题。项目真正发布上线时是一个优秀团队真正开始工作的时候,此刻应该构建自动化系统来快速处理问题并防止对你的用户造成伤害。最后,虽然软件开发有无限的复杂性,深刻理解开发过程是处理好软件开发中的复杂性的基础

关于作者

李鸿毅领导了一个由开发者、设计者和产品经理组成的团队,他们为服务公共福利开发软件。他们的产品包括:

  • Parking.sg:用来替代停车券的App
  • Form.gov.sg:在几分钟内就可以生成一个在线政府表格的网站
  • Data.gov.sg:政府的开放数据仓库

在加入公共服务部门之前,李鸿毅在Google的分布式数据库和图像搜索团队工作,在业余时间,他还做了一些个人项目,比如typographing.com和chatlet.com