为什么优秀的人会搭建糟糕的软件
糟糕的软件是世界上为数不多的无法用金钱解决的问题之一。数十亿美元的航空公司拥有的航班搜索应用程序往往不如学生群体开发的那些应用程序好用。尽管面临着拼车服务的威胁,世界各地的老牌出租车公司还在使用糟糕的预订应用程序。而痛苦的企业 IT 系统通常都是经过多年建设且预算庞大的项目。不管软件糟糕的原因是什么,但它似乎并不是缺乏资金导致的。
令人惊讶的是,软件糟糕的根本原因与特定的工程选择关系并不大,而更多地与开发项目的管理方式有关。最差的软件项目通常是以一种非常特殊的方式进行的:
项目所有者一开始时希望构建一个特定的解决方案,但从未明确指出他们试图要解决的问题是什么。然后,他们从一大群涉众那里收集了一长串的需求。然后,将这个需求列表交给相应的大型外包开发团队,他们从头开始构建这个高度定制的软件。一旦满足所有需求,每个人都会在系统启动并宣布项目完成时庆祝。
然而,尽管该系统在技术上符合规范,但在实际用户手中却发生了严重的问题。它运行缓慢,混乱,有很多莫名其妙的错误,这使得用户使用它充满了挫折。不幸的是,此时外包开发团队已经解散,没有剩余的资源来对其进行必要的 bug 修复。几年后,当一个新项目启动时,所有关于导致这些问题原因的知识都已经离开了组织,恶性循环又重新开始了。
正确的编码语言、系统架构或接口设计将因项目而异。但是,软件的一些特定特性,总会导致传统管理实践的失败,同时它又能让小型初创企业在预算有限的情况下获得成功:
• 软件重用是很容易的;它能帮助我们快速地构建;
• 软件不受构建它所投入资源的限制,而是受它在崩溃之前所能达到的复杂程度的限制;
• 软件的主要价值不在于产生的代码,而在于产生代码的人所积累的知识。
理解这些特性可能并不能保证取得良好的结果,但它确实有助于澄清为什么如此多的项目会产生糟糕的结果。此外,通过失败能推导一些有用的操作原则,这些原则可以极大地提高成功的概率:
开始时越简单越好
发现问题并迭代修复;
尽可能地雇佣最优秀的工程师。
虽然还有很多更微妙的因素需要考虑,但是通过这些原则构建了一个好的基础,可以让你开始构建软件了。
重用软件可以让我们快速构建
软件很容易复制。在机器层面上,代码行可以被复制粘贴到另一台计算机上。通常而言,互联网上到处都是关于如何使用现成的开源代码模块来构建不同类型系统的教程。现代软件几乎从未从零开始开发过。即使是最具创新性的应用程序也是使用现有的软件来构建的,这些软件经过组合及修改以得到新的结果。
可重用代码模块的最大来源是开源社区。开源软件是一种代码可以自由发布给任何人查看和使用的软件。开源社区最大的贡献者几乎都是大型科技公司。如果你想像 Facebook 一样使用最先进的 planet 可伸缩数据库,只需下载他们在 2008 年开源的 Cassandra 代码即可。如果你想亲自体验 Google 的尖端机器学习,请下载 2015 年发布的 TensorFlow 系统。使用开源代码不仅可以加快应用程序的开发速度,还可以让你获得比你自己开发的任何东西都要复杂得多的技术。对于最流行的开源代码来说,它更加安全,因为有更多的人关注并修复漏洞。这就是数字技术取得如此快速进步的原因:即使是最新的工程师也可以利用我们这个行业所提供的最先进的工具。
云服务的出现进一步提高了可重用性,甚至只需要支付订阅费就可以充分使用专有系统。需要一个简单的网站吗?只需使用一个网站构建服务,如 Squarespace 或 Wix,在几次单击中配置一个即可。数据库呢?从 Amazon Web Services 或 Microsoft Azure 订阅一个虚拟服务即可。云服务允许开发人员从专业化中受益;服务提供商负责安装、维护和持续开发一个可靠的、高质量的软件,供其订阅用户使用。这使得软件开发人员不用再在解决问题上浪费时间了,而是更专注于交付实际的价值。
如果我们把所有的时间都花在重建现有技术上,我们就不可能取得技术上的进步。软件工程是关于构建自动化系统的,而首先实现自动化的是常规的软件工程工作。关键是要理解需要重用的正确系统是什么,如何定制它们以满足我们的特定需求,并修复在此过程中发现的新问题。
软件受到复杂性的限制
一个软件能有多大的用处通常取决于它的复杂性,而不是用于构建它的资源数。
IT 系统通常都会有很多功能,但由于它们变得非常混乱,用户仍然讨厌它们。相比之下,排名靠前的移动应用程序往往因其简洁直观而受到称赞。学习使用软件是很困难的。除此之外,对于用户来说新功能实际上会让情况变得更糟,因为累积的复杂性开始变得难以承受。例如,占据苹果媒体生态系统中心位置近 20 年的 iTunes,今年被分成了三个不同的应用(音乐、播客和电视节目),因为它的功能变得过于复杂,一个应用已经无法处理了。从可用性的角度来看,限制不在于可以实现多少功能,而在于什么功能适用于一个简单直观的界面。
即使忽略了可用性,一旦项目变得过于复杂,工程进度也会放缓到停顿。添加到应用程序中的每行新代码都有机会与其他行进行交互。应用程序的代码库越大,在构建新功能时引入的 bug 就越多。最终,由新 bug 造成的工作速度会抵消由功能开发完成的工作速度。这被称为“技术债”,它是专业软件开发的主要挑战。这就是为什么许多大型 IT 系统中都存在多年未解决的问题。向项目中添加更多的工程师只会加剧混乱:当代码库因自身的重量而下降时,它们会在适当的位置运行得更快。
在这种情况下,优化的唯一方法是后退一步,合理化并简化代码库。可以重新设计系统架构,以限制暂时用不着的交互。即使已经构建了非关键功能,也可以删除它们。可以部署自动化工具来检查 bug 和写得不好的代码。Bill Gates 曾经说过,“用代码行来衡量编程进度,就像用重量来衡量飞机制造进度一样”。人类的大脑只能处理有限的复杂性,所以软件的复杂性取决于复杂性预算的使用效率。
构建一个好的软件涉及到扩展和降低复杂性的交替循环。随着新功能的开发,系统中的混乱自然会积聚。当这种混乱开始造成问题时,进度就需暂停,以便花时间清理。这两步过程是必要的,因为不存在柏拉图式的好项目:它取决于我们的需求和我们遇到的实际问题。即使是一个简单的用户界面,比如谷歌的搜索栏,其表面下也隐藏着大量的复杂性,这是无法在一次迭代中完善。我们面临的挑战是管理好这个循环周期,让它变得足够混乱,从而取得有意义的进展,但不要让它变得过于复杂,以至于变得势不可当。
软件不仅仅是编写代码,更是开发知识
在软件开发中,大多数想法都是错误的;这不是任何人的错。只是可能的想法太多了,以至于任何一个特定的想法都可能行不通,即使它是经过非常谨慎和明智地选择的。为了取得进步,我们需要从一堆坏主意开始,抛弃最坏的,并进化出最有前途的。苹果公司,一个富有远见的设计典范,在发布最终产品之前,要经过几十个原型。最终的产品可能看似简单;正是对为什么选择这个特定的解决方案而不是其它替代方案的复杂知识的了解,才使它变得更好。
即使在产品构建完成之后,这些知识仍然很重要。如果一个新的团队接手了一个不熟悉的软件的代码,那么这个软件很快就会开始退化。操作系统将会更新,业务需求将会改变,并且会发现有需要修复的安全问题。处理这些细微的错误通常比构建软件更困难,因为它需要对系统的体系结构和设计原则有深入的了解。
短期内,一个不熟悉的开发团队可以通过权宜之计来解决这些问题。不过,随着时间的推移,由于附加代码的临时性,新的 bug 会不断累积。由于设计模式不匹配,用户界面会变得混乱,系统的整体复杂性增加。软件不应被视为一个静态的产品,而应被视为开发团队集体理解的一种生动表现。
这就是为什么依赖外部供应商进行核心软件开发困难的原因。我们可能会得到一个能运行的系统及其代码,但是关于它是如何构建的以及做出了什么样的设计选择等的宝贵知识会脱离我们的组织。这也是为什么将系统移交给新供应商进行“维护”常常会导致问题的原因。即使系统有很好的文档记录,每次新团队接管时,都会丢失一些知识。多年来,这个系统变成了由许多不同作者编写的代码拼凑而成的。继续运行变得越来越困难;最终,没有人真正理解它的工作原理了。
为了让我们的软件长期保持良好的工作状态,让我们的员工在外部的帮助下学习并在组织中保留关键的工程知识是非常重要的。
好的软件开发的 3 个原则
1. 开始时越简单越好
对于一个特定的领域来说,那些被设定为“一站式服务”的项目往往注定要失败。理由似乎很合理:有什么比让我们的应用程序尽可能多地解决了人们的问题更好的方法来确保我们的应用能解决人们的问题呢?毕竟,这适用于超市等实体店。不同的是,虽然实体店一旦建立起来,就可以相对容易地添加新商品进行销售,但拥有两倍功能的应用程序的构建难度和使用难度都是前者的两倍以上。
构建好的软件需要集中精力:从能够解决问题的最简单的解决方案开始。一个制作精良但相对简单的应用程序在添加必要的功能时从来不会遇到问题。但是,一个大的 IT 系统如果做的很多事情都很糟糕,通常是不可能简化和修复的。即使是像 WeChat、Grab 和 Facebook 这样成功的“一劳永逸”应用程序,一开始时也只是包含非常特定的功能,只有在它们确立了自己的地位之后,功能才得以扩展。软件项目很少因为太小而失败;它们之所以失败是因为太大了。
不幸的是,在实践中保持一个项目的关注点是非常困难的:仅仅从所有涉众那里收集到的需求就能创建一个庞大的功能列表了。
管理这种膨胀的一种方法是使用优先级列表。需求仍会被收集,但是每个需求都会根据它们是绝对关键的、高附加值的、还是最好要有的功能来进行标记。这将创建一个更轻松的规划过程,因为不再需要显式排除功能了。这样,涉众可以更理智地讨论哪些功能是最重要的,而不必担心项目中遗漏了什么。这种方法还明确了是否拥有更多功能的权衡过程。想要增加功能优先级的涉众还必须考虑他们愿意删除哪些功能。团队可以从最关键的目标开始,在时间和资源允许的情况下,按照列表进行工作。
我们所有最成功的应用程序都遵循了类似的流程。 Form.gov.sg 最初是一个手动的 Outlook Macro,我们花了 6 个小时为第一个用户设置,但今天已经处理了大约 100 万份公众提交。Data.gov.sg 最初是一个开源项目的直接副本,现在已经发展到每月超过 30 万次的访问量。Parking.sg 有一个庞大的列表,其中包含了近 200 个我们从未接触过的功能,但是它现在仍然有超过 110 万的用户。这些系统受欢迎,不是因为它们简单,而是因为它(遵循了“开始时越简单越好”的原则)。
2. 发现问题并迭代
事实上,现代软件如此复杂,变化如此迅速,再多的计划也无法消除所有的缺点。就像写一篇好的论文一样,笨拙的初稿对于理解最终的论文是必要的。要构建好的软件,首先需要构建糟糕的软件,然后积极寻找问题以改进解决方案。
首先,要做就是和我们想要帮助的人交谈。我们的目标是了解我们想要解决的根本问题,避免仅仅基于先入为主的偏见而跳转到解决方案上去。当我们刚开始做 Parking.sg 时,我们的假设是,执法人员必须继续对纸质停车券进行心理计算是令人沮丧的。然而,在和一位经验丰富的官员共度了一个下午之后,我们发现,对于专业人士来说,做这些计算其实相当简单。这次谈话为我们节省了几个月的潜在浪费,让我们重新把项目的重点放在帮助司机上。
当心伪装成问题描述的官僚主义目标。“司机在处理停车券时感到沮丧”是一个问题。“我们需要为司机开发一款应用程序,作为我们的部族数字化计划(Ministry Family Digitisation Plans)的一部分”不是一个问题。“在政府网站上查找信息很困难,用户对此感到恼火”是一个问题。“作为数字政府蓝图( Digital Government Blueprint)的一部分,我们需要重构我们的网站,以符合新的设计服务标准”不是一个问题。如果我们的最终目标是让公民生活得更好,我们就需要明确承认那些让他们生活变得更糟的事情。
有一个清晰的问题描述可以帮忙我们通过实验测试不同解决方案的可行性,这些解决方案在理论上很难确定。与聊天机器人交谈可能并不比浏览网站容易多少,而且用户可能不想在手机上安装另一个应用程序,无论它让这个国家变得多么安全。对于软件来说,显而易见的解决方案往往有致命的缺陷,这些缺陷在投入使用之前不会显现出来。我们的目标还不是要开发出最终的产品,而是要首先尽可能快地、廉价地识别出这些问题。用非功能模型模拟测试接口设计。用半功能模型模拟尝试不同的功能。匆忙编写的原型代码可以帮助我们更快地获得反馈。在此阶段创建的任何东西都应被视为一次性的。这个过程的期望输出不是编写的代码,而是更清楚地理解应该构建什么。
对正确的解决方案有了很好的理解,我们就可以开始构建实际的产品了。我们不再探索新的想法,而是将范围缩小到识别与具体实施相关的问题上。从少数测试人员开始,他们将快速发现需要修复的明显错误。随着问题的解决,我们就可以越来越多地向更大范围的用户开放,他们将会发现更深奥的问题。
大多数人只反馈一次。如果你一开始就向一大群人发起,每个人都会给你同样明显的反馈,你将失去方向。即使是由最优秀的工程师开发的最好的产品创意,也会从重大问题开始。这样做的目的是不断优化输出,打磨粗糙的边缘,直到出现一个好的产品。
即使在所有这些迭代之后,产品发布之后的问题仍然是最重要的。在测试过程,可能只有 0.1%的问题不会被注意到。但一旦我们拥有了一百万用户,如果问题得不到解决,每天都有上千个愤怒的人需要我们去安抚。在新移动设备、网络中断或安全攻击对用户造成实质性损害之前,我们需要修复掉这些问题。在 Parking.sg 中,我们构建了一系列辅助系统,不断检查主系统是否存在付款差异、重复的停车会话和应用程序崩溃等。随着时间的推移,构建一个“免疫系统”可以让我们避免因不可避免的新问题出现而造成的不知所措。
总的来说,方法是使用这些不同的反馈循环来有效地识别问题。小的反馈循环允许快速简单的纠正,但会错过更广泛的问题。大的反馈循环可以捕捉到更广泛的问题,但速度慢,成本高。我们希望同时使用这两种方法,尽可能使用紧循环来解决问题,同时仍然使用宽循环来捕获意外错误。构建软件并不是为了避免失败;而是为了尽可能快地在战略上失败,以获取构建好的软件所需的信息。
3. 尽可能地雇佣最优秀的工程师
拥有好的工程技术的关键是拥有优秀的工程师。Google、 Facebook、Amazon、Netflix 和 Microsoft 都运行着数量惊人大型技术系统,然而,众所周知,它们拥有一些最挑剔的面试流程,同时仍激烈竞争,以招募最优秀的候选人。即使是应届毕业生薪水也随着这些公司的发展而大幅上涨,这是有原因的,但这并不是因为他们喜欢烧钱。
Steve Jobs 和 Mark Zuckerberg 都说过,最优秀的工程师的工作效率至少是普通工程师的 10 倍。这并不是因为优秀的工程师编写代码的速度要快 10 倍。而是因为他们能做出更好的决策,才节省了 10 倍的工作量。
一个优秀的工程师对现有软件有更好的把握,可以重用现有软件,从而将他们必须从头开始构建的系统部分最小化。他们更好地掌握了工程工具,自动化了自己工作的大部分常规工作。自动化还意味着解放人类,让他们去解决意料之外的错误,而最优秀的工程师往往更擅长解决这些错误。优秀的工程师自己设计的系统更健壮,也更容易被他人理解。这是一个级联效应,让他们的同事在他们工作的基础上也能更快速、更可靠构建系统。总的来说,优秀的工程师之所以更有效率,并不是因为他们编写了更多的代码,而是因为他们所做的决策可以将我们从无法避免的工作中拯救了出来。
这也意味着,由最优秀的工程师组成的小团队通常可以比由普通工程师组成庞大的团队能更快地完成任务。他们充分利用了可用的开源代码和复杂的云服务,并将日常任务转移到自动化测试和其他工具上,因此他们可以专注于创造性地解决工作中的问题。他们通过区分关键功能的优先级及剔除不重要的工作来快速测试用户的不同想法。这是经典著作《人月神话》的一个中心论点:一般来说,增加更多的软件工程师并不能使一个项目走得更快,只会使它变得更大。
小型的优秀工程师团队也会比大型的普通工程师团队产生更少的 bug 和更少的安全问题。与撰写论文类似,作者越多,在最终的复合产品中需要协调的编码风格、假设和怪癖就越多,从而暴露出更大的潜在问题。相比之下,一个由较小的优秀工程师团队构建的系统将更简洁、更连贯,并且能更好地为其创建者所理解。如果没有简单性,就不可能有安全性,而简单性很少是大规模协作的结果。
一个工程项目的协作性越强,工程师就需要做得越好。工程师代码中的问题不仅影响他的工作,也影响他的同事的工作。在大型项目中,糟糕的工程师最终会为彼此创造更多的工作,因为错误和糟糕的设计选择会像滚雪球一样,造成大量的问题。大型项目需要建立在可靠的代码模块上,并且需要在高效的设计中给出非常明确的假设。我们的工程师越优秀,我们的系统在承受自身重量崩溃之前就能变得越大。这就是为什么最成功的科技公司尽管规模庞大,却坚持要聘用最优秀的人才。系统复杂性的硬限制不是工程工作量的多少,而是它的质量。
结论
好的软件开发首先要对我们想要解决的问题有一个清晰的理解。这使我们需要测试许多可能的解决方案,并收敛到一个好的方法上。通过重用正确的开源代码和云服务,能帮助我们立即访问已建立的软件系统和复杂的新技术,加快了开发速度。开发周期在探索和整合之间交替进行,快速而混乱地开发新的想法,然后集中精力进行简化,以保持复杂性的可控。随着项目的推进,它将不断地受到更大人群的考验,以消除越来越不常见的问题。启动是一个好的开发团队真正的开始工作的时候:应该构建自动化系统层来快速处理问题并防止对实际用户的损害。最终,尽管软件开发有无限的复杂性,但是理解这个过程能为解决如何构建好的软件的复杂性提供基础。
原文链接:
https://www.csc.gov.sg/articles/how-to-build-good-software
评论