为何优秀如程序员,也会构建 Bug 满格的软件?
在 IT 技术圈中,软件具有使用传统管理技术难以构建的特性,这意味着软件开发需要一种不同的、更具探索性和反复性的方法,而这种具体的措施又该如何实施?
在本文中,毕业于 MIT 计算机系、曾在 Google 做过 PM、后来担任新加坡公民服务学院科技团队负责人的 LI HONGYI(音译:李鸿毅)将从自身多年经验出发,分享构建一套优秀软件的完整体系。
而提及李鸿毅本身,其可谓是出身非 IT 圈的 IT 之家,其母亲何晶拥有新加坡国立大学电子工程专业的一等学位和斯坦福大学的电子工程硕士学位;其弟弟也是 MIT 计算机系毕业,在 Scala 圈子里颇有名气;而他的父亲也曾在 Facebook 主页 Po 出过一段复杂的 C++ 程序代码,可见他的 C++ 基础并不一般,且其还声称退休后打算读一下 Haskell 教科书,另外,他的父亲也就是当今的新加坡总理李显龙。
作者 | LI HONGYI
译者 | 弯月,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
这个世界上金钱无法解决的事情为数不多,然而糟糕的软件就是其中之一。身价数十亿美元的航空公司做出来的航班搜索应用往往还不如学生的作品。尽管全世界成熟的出租车公司都面临着乘车共享服务的威胁,但他们的叫车应用依然很糟糕。痛苦的企业IT系统通常需要投入大量预算,花多年的时间才能建立起来。引发劣质软件的原因各种各样,但肯定不是缺乏资金。
令人惊讶的是,引发劣质软件的根本原因似乎与工程上的抉择关系不大,更多的是与开发项目的管理方式有关。糟糕的软件项目的过程往往非常相似:
项目所有者想要构建某个解决方案,但绝不会明确定义希望解决的问题。然后,他们会收集一长串大量利益相关者的要求。然后,将此列表交给大型的外部开发团队,由这个团队从头构建这个高度定制的软件。等到所有需求都得到满足后,每个人都会庆祝系统启动,并宣布项目完成。
然而,尽管这样的系统符合技术上的规范,但真正将它交到实际用户手中时就会出现严重的问题。运行速度缓慢,使用方法含混不清,而且还塞满了各种bug,最后只落得屡屡滑坡。不幸的是,到这个地步的时候,外部的开发团队已然解散,已没有剩余资源来进行必要的修复。等到几年后,一个新的项目又启动了,所有了解引发这些问题原因的人都已经离职,于是悲剧又一次开始重复上演。
正确的编程语言、系统架构或界面设计因项目而异。但是,软件特有的一些特征会导致传统的管理实践失效,同时小型创业公司能够以低廉的预算取得成功:
重用好的软件很容易,这是快速构建优秀软件的捷径之一;
软件的限制不在于构建它的资源量,而在于它在崩溃之前的复杂程度;
软件的主要价值不在于生成的代码,而在于生成代码的人员积累的知识。
虽然了解这些特征可能无法保证良好的结果,但确实有助于理解为什么这么多项目会产生不良后果。此外,我们还可以总结出如下核心的运营原则,从而大大提高成功的机会:
项目刚开始的时候越简单越好;
找到问题并进行迭代;
聘请最优秀的工程师。
虽然还有很多更微妙的因素需要考虑,但这些原则为我们构建优秀的软件奠定了基础。
借助重用快速构建优秀的软件
软件的复制方便易行。从机器的角度来看,你可以将代码逐行复制并粘贴到另一台计算机上。更一般的情况,互联网上提供了很多教程,你可以在线学习如何利用现成的代码模块构建不同类型的系统。现代软件几乎没有一个是从头开始开发的。即使是最具创新性的应用程序也是通过融合和修改现有的软件而构建。
可重用代码模块最大的来源是开源社区。开源软件的代码可供自由发布,以及任何人的查看和使用。开源社区最大的贡献者都是巨型科技公司。如果你想跟Facebook一样使用最先进的可扩展数据库,那么只需下载他们于2008年开源的Cassandra代码。如果你想亲自试试Google尖端的机器学习,那么可以下载他们于2015年发布的TensorFlow系统。使用开源代码不仅可以加快应用程序的开发速度,而且还可以让你接触比你自己开发的任何技术都复杂百倍的技术。此外,流行的开源代码更加安全,因为关注它们以及修复它们的漏洞的人也更多。这就是数字技术发展如此迅速的原因:即使是新手工程师也可以利用我们提供的最先进的、最专业的工具构建软件。
云服务的出现进一步提高了可重用性,你只需支付订阅费即可使用系统,甚至拥有专门的系统。你需要一个简单的网站?那么只需使用Squarespace或Wix等网站的构建服务,轻轻点击几下即可配置一个。你想要一个数据库?那么只需订阅AWS或微软的Azure。开发人员可以通过云服务享受专业化的服务,由服务提供商处理设置和维护的工作,并持续为所有的订阅用户开发可靠、高质量的软件。如此一来,软件开发人员就无需在这些问题上浪费时间,可以专心提供实际的价值。
如果我们把所有时间花在重建现有的技术上,那么就无法取得技术上的进步。软件工程的工作是构建自动化的系统,而我们应该自动化的第一件事就是日常的软件工程工作。现在问题的关键在于了解我们需要重用哪些系统,如何根据自身的独特需求改造这些系统,同时在这个过程中解决新发现的问题。
软件受到复杂性的限制
软件的功能性往往会受到复杂性的限制,却与投入的资源量没有太大关系。
通常IT系统都有很多功能,但仍然无法博得用户的喜爱,因为这些系统非常混乱。相比之下,名列前茅的移动应用往往因其简单性和直观性而备受称赞。学习使用软件很难。除此之外,实际上用户并不喜欢新功能,因为日益积累的复杂性终有一日会决堤。例如,iTunes在担任苹果的媒体生态系统中心近20年后,于今年分裂成了三个不同的应用程序(分别服务于音乐、播客和电视节目),因为对于一个应用而言iTunes的功能过于复杂。从实用性的角度来看,限制不是说可以实现多少个功能,而是说哪些才是简单直观的功能。
即便抛开实用性不提,一旦项目变得过于复杂,工程进度就会停滞不前。向应用程序添加每一行新代码都需要考虑与其他代码的交互。应用程序的代码库越大,构建新功能时引入的bug就越多。最终,bug的产生速率会超越新功能的开发速率。这就是赫赫有名的“技术负债”,也是专业软件开发的主要挑战。这就是为什么许多大型IT系统中依然存在多年未解决的问题的原因。向项目添加人手只会让局面更混乱:由于代码库无法承其重而崩溃,所以这些开发人员只能在原地打转。
在这种情况下,唯一方法是以退为进,改善并简化代码库的结构。重新设计系统架构,防止意外的交互。有些非关键的功能,即便是已然构建完毕,也可以将其删除。另外,我们还可以部署自动化工具来检查bug和编写不良的代码。比尔·盖茨曾说过:“用代码行数来衡量编程的进度,就如同用重量来衡量飞机的制造进度。”人类思维只能处理有限的复杂性,因此软件系统的复杂程度取决于如何有效地利用这些复杂度。
构建良好的软件涉及扩展与降低复杂性的循环交替。随着新功能的开发,系统中自然会积累混乱。当这种混乱开始引发更大的问题时,你就需要暂停进度,花时间清理混乱。这两个步骤都是必要的过程,因为这个世界上没有柏拉图式的工程:软件工程取决于你的需求和实际遇到的问题。即使是非常简单的用户界面(比如Google的搜索栏),表面下也有可能包含大量的复杂性,你无法在一次迭代中完善这些复杂性。难点在于如何管理这个循环,在开发新功能的时候允许适度的复杂度,同时又不能让复杂度过度积累以至于变得势不可挡。
软件的核心是积累知识而不是编写代码
在软件开发中,大多数想法都很糟糕,但这不是任何人的错。只不过人们的各种想法实在太多了,所以某些想法肯定会行不通,即使非常谨慎和明智地抉择,也再所难免遭遇失败。所以为了顺利地推进项目,你需要从一堆糟糕的想法中摈弃最坏的想法,并发展最有希望的想法。苹果就是一个富有远见的设计典范,但在最终的产品问世之前,它也经历了几十个原型。最终的产品看似简单,但其中蕴含着错综复杂的知识,为什么他们选择了这个特定的解决方案?为什么其他替代方案不可行?
即使在产品构建完成之后,这种知识仍然很重要。假设一个新团队接管了一个不熟悉的代码库,然后很快开始升级该软件。操作系统会更新,业务需求会发生变化,而且还会发现需要修复的安全问题。处理这些复杂的错误往往比构建软件本身的难度都大,因为你需要要对系统的体系结构和设计原则有深入的了解。
在短期内,一个不熟悉的开发团队可以通过权宜之计修复这些问题。但随着时间的推移,由于新添加代码的权宜性,新的bug会不断积累起来。由于新代码不符合设计范例,用户界面会变得混乱,而且整体的系统复杂性也会激增。所以,我们不应该将软件视作静态产品,而是应该当成团队集体理解能力的生动体现。
这就是为什么你很难依靠外部供应商为你开发核心软件的原因。你可以拿到一个运行良好的系统以及代码,但有关该软件的构建及设计抉择的宝贵知识却流失了。这也是为什么将系统交给新的供应商进行“维护”时往往会引发各种问题的原因。即便这个系统有良好的文档记录,但每次交给新团队接管时都会遗失一部分知识。多年以后,该系统就会因为不同代码的拼凑而变得千疮百孔。长此以往,系统的运行就会越来越难,因为没有人真正理解系统的运作。
因此,为了保证软件的长期良好运行,让你的员工与外部的帮手一起学习系统的知识,将关键性的工程知识保留在组织内部,这极其重要。
开发优秀软件的三个原则
1.项目刚开始的时候越简单越好
对于特定的领域而言,“一站式商店”的项目往往注定会失败。其中的原因很明显:确保你的应用可以为用户解决实际问题,还是解决尽可能多的问题,孰重孰轻?毕竟,“一站式商店”就如同超市一般的实体店。不同之处在于,虽然在实体店建立后添加新商品相对很容易,但是构建拥有两倍功能应用的难度远远不止两倍,而且很难使用。
构建优秀的软件时需要关注:以解决某个问题的最简单方案为起点。一个设计精心又简洁的应用永远不会遇到添加功能的问题。但是,一个大型的IT系统虽然能够解决很多问题,却往往无法简化或修复。即使是成功的“包罗万象”的应用,比如微信、Grab和Facebook,刚开始时也有非常具体的功能,而且只有在确保了他们的地位后才开始进行扩展。软件项目的失败很少可以归因于规模太小,它们的失败往往由于规模太大。
遗憾的是,在实践中保持项目聚焦重点是一件非常困难的事情:单单是收集所有利益相关者的需求就会得到一份巨长的功能列表。
管理这种膨胀的一种方法是使用优先级列表。我们仍然需要收集所有的需求,但每个需求都有相应的标记:是绝对性的关键功能、高附加值还是非常有帮助性。这种方式有助于建立一个压力较小的计划流程,因为你不需要再明确驳回某些功能。而且利益相关者也可以更加理智地讨论哪些功能才是最重要的,同时也不必担心项目遗漏了某些问题。此外,这种方法也明确了添加更多功能的权衡。即便利益相关者想要提高功能优先级,他们也必须考虑愿意优先考虑哪些功能。而开发团队则可以从最关键的目标开始,在时间和资源允许的情况下沿着优先级列表逐个实现。
我们所有成功的应用开发都采用了类似的流程。Form.gov.sg刚开始的时候只是一个手动的Outlook宏,我们只花了6个小时为我们的第一个用户设置了这个宏,如今这个系统已经处理了大约一百多万个公共提交的请求。Data.gov.sg刚开始的时候只是复制了一个开源项目,发展到如今每月的访问量已经超过了30万次。Parking.sg曾经有200多个等待构建的庞大功能列表,但我们从来都没有构建过这些功能,可如今仍有超过110万的用户。这些系统虽然简单,却也正是因为简单才受到好评。
2.找到问题并进行迭代
事实上,现代软件的结构如此复杂,变化如此之快,以至于我们无法通过良好的计划消灭所有缺陷。如同撰写一篇好文章一样,即便早期的草稿非常尴尬,但我们只能通过这些草稿了解最终的论文。要想构建优秀的软件,首先需要构建糟糕的软件,然后再积极寻找问题并改进解决方案。
刚开始的时候,你只需要跟需要帮助的人交谈。了解你需要解决的根本问题,并避免仅凭先入为主的偏见早早提出解决方案。当年,我们刚启动Parking.sg项目的时候,我们曾假设执法人员很难坚持心算纸质的优惠券。然而,在与某位经验丰富的官员交谈了一个下午之后,我们发现,对于专业人士来说,这些计算实际上非常容易。此次谈话为我们节省了数月的潜在工作,并让我们将项目重点放在了帮助司机上。
谨防伪装成问题陈述的官僚目标。比如:
“驾驶员在处理停车券时感到很沮丧”:是一个问题。
“作为政府部门家庭数字化计划的一部分,我们需要为司机构建应用”:不是问题。
“用户对很难在政府网站上查找到信息而感到恼火”:是一个问题。
“作为数字政府蓝图的一部分,我们需要重建我们的网站以符合新的设计服务标准”:不是问题。
如果我们的最终目标是让公民的生活更美好,那么我们就需要清楚地了解他们生活中的困难。
明确的问题陈述可以让你通过实验来测试不同解决方案的可行性,因为你很难仅通过理论确定解决方案。与聊天机器人交谈的难度可能超过了浏览网站,用户可能不想在他们的手机上多装一个应用,无论该软件对于国家的安全有多么重要。在开发软件的时候,显而易见的解决方案通常都带有致命的缺陷,但只有投入实际使用你才能发现。所以,你的目标不是构建最终产品,而是首先通过快速且廉价的渠道找到这些问题。你可以利用非功能性的模型来测试界面设计,利用半工作的模型尝试不同的功能,快速编写原型代码更快地获得反馈。这个阶段创建的所有东西都是一次性的。这个过程所需的输出不是编写的代码,而是更清楚地了解哪些才是应该构建的功能。
在对正确的解决方案有了充分的理解之后,你可以开始构建实际产品。这时,你应该停止探索新想法并设法缩小范围,以确定有待实现的特定问题。你可以从少数测试人员开始,他们很快就会发现需要修复的明显错误。随着一个个问题得到解决,你可以逐步扩大范围,让他们发现更多深奥的问题。
大多数人只会提供一次反馈。如果你上来就将软件公开给大量用户,那么每个人都会为你提供最显而易见的雷同反馈,以后你就没办法再收集反馈了。即便再优秀的工程师构建再好的产品创意也会出现重大问题。所以我们的目标是反复改进,打磨边缘案例,直到创建出最好的产品。
即使在完成所有迭代并发布软件之后,产品的问题也很重要。有些问题的发生几率只有0.1%,测试期间没有被注意到。但是,一旦你有一百万的用户,那么每天都会面临一千个愤怒的用户需要安抚。你需要抢在给用户造成重大损害之前,修复新移动设备、网络中断或安全攻击造成的问题。我们在Parking.sg中构建了一系列辅助系统,可以不断检查主系统是否存在付款差异、重复停车会话和应用程序崩溃等问题。随着时间的推移,建立一个“免疫系统”可以让你避免不堪重负,因为新的问题必然会不断涌现。
总的来说,我们的基本思路是利用不同的反馈回路来有效地识别问题。小的反馈回路可以快速找到问题并轻松修复,但会错过更广泛的问题。大型反馈循环可以捕捉到更广泛的问题,但其速度慢、开销大。你可以结合两者,利用紧凑的循环解决尽可能多的问题,同时利用大型循环捕捉意外的错误。构建软件不是为了避免失误,而是应该有策略地快速失败,然后再获取构建优秀软件所需的信息。
3.聘请最优秀的工程师
建立优秀的工程团队的关键在于拥有优秀的工程师。谷歌、Facebook、亚马逊、Netflix和微软拥有全球最大的令人眼花缭乱的技术系统,然而,虽然他们面临招聘优秀人才的剧烈竞争,但他们仍然拥有最苛刻的招聘流程。应届毕业生的工资随着这些公司的发展壮大而节节升高,这是有一定原因的,当然不是因为这些公司不在乎钱。
史蒂夫·乔布斯和马克·扎克伯格都表示,最优秀的工程师的工作效率至少是普通工程师的10倍。这不是因为优秀的工程师编写代码的速度是普通人的10倍,而是因为他们能够做出更好的决定,可以节省10倍的工作量。
优秀的工程师更好地掌握了他们可以重复使用的现有软件,从而最大限度地减少他们必须从头开始构建系统的工作量。他们更好地掌握了工程工具,能够自动化大部分的日常工作。自动化还可以将人类解放出来,让他们去处理意外的错误,而优秀的工程师在这个方面有着优异的表现。优秀的工程师设计的系统更健壮,更方便他人理解。这具有乘数效应,因为他们可以帮助同事更快更可靠地完成各自的工作。总的来说,优秀的工程师的效率非常高,不是因为他们可以编写更多的代码,而是因为他们做出的决定可以将你从不必要的工作中解脱出来。
这也意味着,优秀的工程师组成的小型团队构建软件的速度远远超过了普通工程师组成的大型团队。他们可以充分利用现成的开源代码和复杂的云服务,并利用自动化测试及其他工具摆脱单调的日常工作,让每个人都专心思考如何创造性地解决问题。他们可以确定关键功能的优先级,并减少不必要的工作,快速测试用户的不同想法。这是经典著作《神话人月》的核心论点:总的来说,增加软件工程师对加快项目速度并无裨益,只会让项目的规模扩大。
与普通工程师组成的大型团队相比,优秀的工程师组成的小型团队制造的bug和安全问题也会更少。与撰写论文类似,编写代码的人越多,编程风格就越多,假设和怪癖就越多,最终只能在拼凑而成的产品中协调,因此出问题的几率就越大。相比之下,由优秀的工程师组成的小型团队构建的系统将更加简洁、连贯,而且更加便于理解。没有简单性就无法保证安全性,大规模的协作很难铸就简单性。
工程工作的协作性越高,我们就越需要优秀的工程师。工程师代码中的问题不仅会影响到他个人的工作,还会影响到身边同事的工作。在大型项目中,糟糕的工程师只会给彼此添乱,因为错误和糟糕的设计就像滚雪球一样,越滚越大,直到出现重大问题。大型项目需要建立在可靠的代码模块上,并采用有效的设计,同时还需要非常明确的假设。你的工程师越优秀,系统在无法承其中而崩溃之前发展得就越大。这就是为什么成功的科技公司尽管规模庞大但坚持招聘优秀的人才。系统复杂性的硬性限制不在工作量,在乎质量之间也。
总结
良好的软件开发始于对需要解决的问题有清楚的认识。你可以测试许多解决方案,并找到最佳方案。通过重用正确的开源代码和云服务,以及使用现成的软件系统和复杂的新技术,来加速开发。开发周期在探索和整合之间交替,在控制一定的混乱的同时快速地开发新功能,然后集中和简化,以保证复杂度的可管理性。随着项目的进展,接受更多用户群的测试,以消除日益罕见的问题。只有当一个优秀的开发团队真正步入正轨之后才能正式启动项目:你应当构建多层自动化系统,快速处理问题,并防止对实际用户造成伤害。最后,尽管软件开发存在无限的复杂性,但理解这一过程为解决如何构建优秀软件的复杂性提供了基础。
原文:https://www.csc.gov.sg/articles/how-to-build-good-software
作者:LI HONGYI,新加坡公民服务学院科技团队负责人,前Google员工,从事分布式数据库与图像搜索。
评论