“技术债”在软件工程领域是一个贬义词。这句话常常带着遗憾的意味:过去的错误最终需要通过重构来弥补。如果我们将技术债定义为未来必须完成的工作,那么我们可以根据未来将要花费的工作时间估算成本。我们也可以把时间“投资”在完成现在的工作上。这种思维模式帮助我们避免了很多愚蠢的消耗,本文介绍了几个关于采用技术债方式帮助项目成功的案例。
“技术债”在软件工程领域是一句“脏话“。这句话常常带着一种遗憾的气氛:过去的错误最终需要通过重构来弥补。
同样是债务,金融债并未受到普遍诟病。你的朋友用抵押贷款买房,你会怎么说呢?恭喜你!债券是基础设施和公共工程的标准融资形式。企业使用各种各样的债务,而华尔街仍表现出对股价上涨的信心。
两者的不同之处在于意图。如果技术债并非总是由错误假设和意外情况造成的事故呢?你将如何使用技术债抵押贷款呢?
如果我们将技术债定义为未来必须完成的工作,那么我们可以根据未来将要花费的工作时间估算成本。我们也可以把时间“投资”在完成现在的工作上。
这种思维模式帮助我避免了很多愚蠢的开支,也避免了支付我负担不起的系统维护费用,这也让我看到了有意使用技术债的机会。这里有几个关于采用技术债方式帮助项目成功的案例。
脚手架
每当我决定要对一个大型项目的主要部分进行优先级排序时,首要目标是先验证项目中存在最大风险的部分。我说的风险,是指那些最容易出错的部分,因为它们很复杂、难以定义且难以理解,或者有其他一些“龙出没”的味道。因为这些组件是项目中最难构建的部分,所以我会在代码库还比较小更容易演进的时候优先构建它们。
然而,仅仅构建存在风险的组件是远远不够的。我需要确保我构建的这些风险组件是正确的。除非尝试使用,否则我们永远不会知道它是否正确,所以我会尽快上线代码。这通常意味着在应用程序的其余部分构建完成之前,需要找到一种方法,使风险组件可用。
在我们团队开发 Squarespace Email Campaigns 的过程中,这意味着首先要构建电子邮件编辑器。Squarespace 以一流的设计和强大的内容编辑工具而闻名,我们希望打造出能够在竞争中脱颖而出的产品。几个月后,我们有了一个编辑器,并且很高兴的向同事们展示了,但此时我们还没有搭建出能真正发送电子邮件的系统。如果编辑器只是一个不能发送电子邮件的玩具应用程序,我们就不能得到我们需要的真实反馈。
最终产品需要能够快速可靠地发送数亿封电子邮件,但现在我们不需要这种能力。
所以我们会问自己 :“将编辑器提供给我们的同事使用,最简单的方法是什么?“当时 Squarespace 只有几百名员工,内部人员测试可以容忍些许的不可靠。搭建一个能够非可靠地发送几百封电子邮件的东西比搭建我们所考虑的健壮系统要容易得多。我们意识到,我们可以建造一些低成本且日后可以丢掉的脚手架,来更快地获得用户的反馈。
我们制定了几项指导方针来帮助我们顺利地推行“搭建脚手架”计划:
我们按照预估成本行事。如果我们不能在预计的时间内完成脚手架,这就表明我们引入了太多的复杂性,需要重新考虑方案。
它是以会被丢弃为前提设计的,并从一开始就这样进行交流。代码被很好地控制,并且我们没有浪费时间在细微的实现细节上吹毛求疵。
我们了解脚手架的局限性,并避免在发生故障会造成损害的情况下使用它。如果我们花时间清理脚手架会造成错误,我们将会陷入比预期更严重的债务中,因此我们会非常认真地规划使用脚手架的测试情况。
利益相关方知道我们是在有意负债。我们会向利益相关方和用户像宣传功能一样说明这些限制,如“看看我们节省了多少时间!”,在这个过程中,确保让他们知道我们以后需要投入时间来构建真正的电子邮件发送系统。
通过临时替换依赖项,脚手架可以帮助我们调整构建应用程序组件的顺序,这样我们就不会在有办法验证它们之前卡在程序组件的构建了。用后可弃的电子邮件发送器在我们还清晰记得代码时就帮我们获得了关于电子邮件编辑器的反馈,帮助我们更快地迭代和完善我们最困难的问题。当需要构建真正的电子邮件发送器时,我们也从构建脚手架中吸取了一些经验教训。
硬编码
我最近开发的一个产品,在仪表盘上提供了一个周期性的消息展示区域,例如:“祝您的订阅者节日快乐”。每隔一两个月,我们团队就会安排在仪表盘的这个区域展示新的内容。
我们需要一个内容管理系统( Content Management System,CMS)来让我们的产品负责人(PM)和设计师自己更新仪表盘上的消息。Squarespace 毕竟是一家 CMS 公司!此外,PM 通常希望仪表盘在特定的时间进行更新,工程师也不希望根据这些需求手动安排上线发布的时间。
一种方法是构建一个基于数据库的 CMS,让我们团队的各个成员能够即时更新仪表盘的内容,而无需工程师的干预。
不幸的是,这种方法有一定的成本:需要管理额外的前端、验证和数据。这些隐藏的成本是一种不良的技术债,它给未来的开发人员带来了比功能本身更大的维护工作量。此外,我们预计仪表盘上的消息内容将持续数周不变,因此我们不会从提供这种即时更新的解决方案中获益。
因此,我们没有采用基于数据库的 CMS,而是将内容存储在一个 YAML 文件中,并重用现有的工具来提供 CMS 功能。变更仪表盘消息的用户界面由原来的浏览器 UI 替换为文本编辑器和 Git。其他团队成员可以通过代码审核的方式来验证这些变更,因此我们不需要花费时间编写验证和错误处理程序。内容变更通过我们现有的 CI 工具在不同环境中自动同步。
在特定的时间发布新内容需要更多的硬编码才能实现。比如:
在决定是否需要硬编码时,我会考虑如下几个因素:
变更几个小时之后才能生效,是可以接受的吗?硬编码有一个明确的限制:变更需要将新代码部署到生产环境中。如果忽略该限制,可能会给运维带来麻烦,造成不良的技术债。
可以接受 Git 作为更新用户界面吗?更新将需要与团队的版本控制工具(可能还需要和 CI 系统)进行交互。为了避免引入额外的协调或培训开销,我们还需要确认项目中是否已经有人了解 Git 了?
是否存在现有的 UI、验证和数据模式?无需定义新模式是硬编码能带来的最大好处。如果我们的应用程序已经有了可以清晰地解决问题的模式,那么硬编码可能就没有缺点了。
通常,可以考虑使用硬编码的其他模式还包括允许列表、表单字段选项和特性标志等。
不修复所有边缘情况
我曾经必须构建一个功能:允许用户最多创建 10 个条目,但一定不能超过 10 个。这里有一个常见的竞态条件:如果我们几乎是同时发出了两个 create-item 请求,这两个请求都将计算现有的条目数,然后两个请求都将创建对应的条目。如果在已经有了九个条目的情况下这样做,就可以突破该限制,最终得到 11 个条目。
大多数 QA 测试人员会认为这是一个 bug,而 bug 只是用户可以看到的技术债。
数据库事务可以帮助解决该问题,但我们使用的是不支持事务的 NoSQL 数据库。读/写锁也可以提供帮助,但我们是在分布式系统中操作,所以我们需要使用分布式锁。如果我们通过编写一系列加锁的代码来“完美”实现该限制条件,那么最终可能会引入新的、意想不到的 bug。我们必须花时间编写一系列加锁的代码,而在我们之后从事该工作的每个开发人员都必须理解这些加锁的代码。
这会让人觉得,为了防止用户多创建一个条目付出了过大的精力。非技术性的解决方案是怎样的呢:如果我们故意不修复竞态条件呢?
此时我不是说悄悄地把它留在那里让下一个开发人员来处理!故意在这里意味着需要解决以下问题:
当有 11 个条目时会发生什么?嗯,实际上并没什么。这是一个随意的限制,应用程序的其余部分并不关心这些额外的条目。
相比正常情况,这种竞态条件触发的频繁吗?是的,一个简单的数据库查询查到了超过 10 个条目的帐户,并且每个条目记录都有一个时间戳。结果表明,这种竞态条件在生产中并没有发生。
我们最好不偿还这项技术债。因为创建额外条目的影响很低,并且易于监控,所以我们可以将时间花在更高优先级的工作上,而不是解决在实际中并不是问题的问题。
良性的技术债是故意留下的
很多不良的技术债来自于过多的构建,以及在维护和 bug 修复上花费了比预期更多的时间。这就像是买了太多的房子,最后却陷在了资不抵债的抵押贷款中。
关键是要有意识地考虑你所投入的时间,及你所能承担的成本。构建的太少也是错误的,因为你以后总是要构建得更多。构建那些易于丢弃和替换的东西;它将使代码更加模块化。良性技术债有明显的、众所周知的局限性。将这些记录在代码注释、自述、常见问题解答和与关心这些问题的人的对话中。
如果使用得当,良性技术债可以帮助你把时间集中在最重要的事情上,从而更快地构建软件。
评论