第 3 章 能工作的代码是不够的
(战略式编程与战术式编程)
好的软件设计中最重要的要素之一是你在执行编程任务时所采用的思维方式。许多组织都鼓励采取战术式(Tactical)的思维方式,着眼于尽快让功能正常运行。但是,如果你想要一个好的设计,就必须采取更具战略性(Strategic)的方法,投入时间做出整洁的设计并解决问题。本章讨论为什么从长远来看,战略式方法既能产生更好的设计,实际成本又比战术式方法更低。
本章概览能工作的代码,为什么还不够?
3.1 战术式编程
大多数程序员以我称为战术式编程的思维方式来进行软件开发。在战术式方法中,你主要关注的是让代码正常工作,例如实现新功能或修复代码缺陷。乍一看,这似乎是完全合理的:还有什么比编写能工作的代码更重要的呢?但是,战术式编程几乎不可能产生良好的系统设计。
战术式编程的问题在于它是短视的。如果你是战术式编程人员,那么你会设法尽快完成任务。也许你有必须遵守的截止期限,因此不会优先考虑为未来做计划。你不会花费太多时间来寻找最佳设计。你只想尽快让代码工作起来。你告诉自己,增加一些复杂性或引入一两处蹩脚的小实现,如果可以使当前任务更快地完成,那就没什么大不了的。
系统就是这样变得复杂的。如上一章所述,复杂性是逐步积累的。系统变得复杂,不是某个特定因素造成的,而是几十或数百个小问题积累的结果。如果你在编码时总是使用战术式思维方式,那么每个编程任务都会带来一些此类复杂性。为了快速完成当前任务,每增加一点复杂性似乎都是合理的折衷。但是,复杂性会迅速累积,尤其是每个人都使用战术式编程的时候。
不久之后,某些复杂性将开始引起问题,你也会开始后悔当初走了捷径。但是,你会告诉自己,使下一个功能正常工作比回头重构现有代码更为重要。从长远来看,重构可能会有所帮助,但是肯定会拖慢当前任务的进度。因此,你会通过快速修补程序来解决遇到的问题。这只会增加更多的复杂性,继而需要更多的补丁。很快,代码就变得一团糟,到了这个地步,清理它需要花费数月的时间。你的日程安排无法承受这种延误,只解决一两个问题似乎也于事无补,因此你就只能继续保持这种战术式的编程方式。
如果你长期参与大型软件项目,我想你很可能已经在工作中见过战术式编程,也遇到过它所导致的问题。一旦走上战术式编程的道路,就很难改变。
几乎每个软件开发组织都有至少一个将战术式编程发挥到极致的开发人员:战术龙卷风。战术龙卷风是一位多产的程序员,他产出代码的速度比其他人快得多,但完全以战术方式工作。需要快速实现某项功能时,没有人能比战术龙卷风更快地完成任务。在某些组织中,管理层将战术龙卷风视为英雄。但是,战术龙卷风所到之处一片狼藉。将来必须使用其代码的工程师很少会把他们视为英雄。通常,其他工程师必须清理战术龙卷风留下的混乱局面,这使得那些工程师(他们才是真正的英雄)的进度似乎比战术龙卷风慢。
译者图解一次捷径如何变成持续负担?
3.2 战略式编程
成为一名优秀的软件设计师的第一步是要意识到 能工作的代码是不够的。引入不必要的复杂性以更快地完成当前任务是不可接受的。最重要的是系统的长期结构。任何系统中的大多数代码都是在扩展现有代码库时编写的,因此,作为开发人员,最重要的工作就是让未来的扩展更容易。尽管你的代码当然必须能工作,但你不应将“能工作的代码”视为主要目标。你的主要目标必须是做出出色的设计,同时让代码正常工作。这就是 战略式编程。
战略式编程需要一种投资的思维方式。你必须花费时间来改进系统的设计,而不是采取最快的方式来完成当前的项目。这些投资会在短期内让你放慢脚步,但从长远来看会加快你的速度,如图 3.1 所示。
有些投资是主动进行的。例如,值得花一些时间为每个新类找到一个简单的设计,而不是直接去实现第一个想到的办法。请尝试几种替代设计并选择最整洁的设计。设想一下将来可能需要对系统做出哪些更改,并确保你的设计会让这些更改更为容易。编写好的文档是主动投资的另一个例子。
其他投资则是对已发现问题的应对。无论你预先投入多少,设计决策中都不可避免地会出现错误。随着时间的推移,这些错误将变得显而易见。发现设计问题时,不要忽略它或只是对其进行简单的修补。花一些额外的时间来真正修复它。如果你进行战略式编程,就会不断对系统设计进行小幅改进。战术式编程则恰恰相反:你不断增加复杂性,而这些复杂性将来会引起问题。
3.3 该投资多少?
那么,投入多少才合适?大量的前期投资(例如尝试一次性设计整个系统)并不有效。这是瀑布式方法,我们知道它不起作用。随着你对系统的理解不断深入,理想的设计会逐渐涌现出来。因此,最好的方法是持续进行小额投资。我建议你将总开发时间的大约 10% 到 20% 用于投资。这个比例足够小,不会对你的日程安排产生重大影响,但又足够大,可以随着时间的推移产生重大收益。因此,你的初始项目将比纯战术式方法多花费 10% 到 20% 的时间。额外投入的时间将带来更好的软件设计,并且你将在几个月内开始体会到这些好处。不久之后,你的开发速度将比战术式编程快至少 10 到 20%。到这个时候,你的投资就不再需要额外成本了:过去的投资所节省的时间将足以支持未来的投资。你将迅速收回初始投资的成本。图 3.1 说明了这种现象。
图 3.1:一开始,战术式编程方法将比战略式方法更快地取得进展。但是,在战术式方法下,复杂性积累得更快,从而降低了生产率。随着时间的推移,战略式方法会取得更多进展。注意:此图仅用于定性说明,我尚未见到关于这些曲线精确形状的实证数据。
相反,如果你进行战术式编程,则可以将初始项目的完成速度提高 10% 到 20%,但是随着时间的推移,复杂性的累积会降低开发速度。不久之后,你的编程速度至少会降低 10% 到 20%。你将很快耗尽起初节省的所有时间,并且在系统此后的生命周期中,与采用战略式方法相比,你的开发速度将更加缓慢。如果你从未碰到过质量非常糟糕的代码库,请与有经验的人谈一谈。他们会告诉你糟糕的代码质量会使开发速度至少降低 20%。
术语 技术债 经常被用来描述战术式编程所导致的问题。通过战术式编程,你将从未来借用时间:现在的开发会变得更快,但以后会更慢。就像金融债务一样,你偿还的金额将超过你借用的金额。与金融债务不同,大多数技术债永远不会完全偿还:你将不断偿还,直到永远。
图 3.1 提出了一个重要的问题:战略式曲线和战术式曲线的交叉点在哪里?换句话说,战略式方法要多久才能收回成本?不幸的是,我不知道是否有任何关于这个问题的数据,而且很难通过有说服力的受控实验来回答这个问题。我个人认为,收回成本需要 6 到 18 个月。这很大程度上与开发人员的记忆力有关:一段代码写完几个月之后,开发人员已经忘记了编写时的大部分思路,所以如果代码很复杂,开发速度会显著下降。这些额外的成本很快就会抵消战略式编程最初带来的收益。同样,这只是我的个人意见,我没有任何数据来支持它。
译者注(原文疑点):本段原文写的是“战略式编程最初带来的收益”。结合前文“战术式编程先省时、后付出额外成本”的论述,此处疑应为“战术式编程”。正文保留原文表述。
3.4 初创公司与投资
在某些环境中,现实压力会阻碍战略式方法的实施。例如,初创公司在早期面临巨大的压力,需要尽快发布产品的早期版本。在这些公司中,10% 至 20% 的投资似乎也负担不起。结果,许多初创公司采取了战术式方法,在设计上花费的精力很少,在问题出现后花在清理上的精力更少。他们为此辩解说,只要公司成功了,就有足够的钱聘请更多工程师来清理问题。
如果你正在一家朝着这个方向发展的公司工作,就应该意识到,一旦代码库像意大利面条一样纠缠不清,就几乎不可能修复。你可能会在产品的整个生命周期内付出高昂的开发成本。此外,好设计的收益或坏设计的代价很快就会显现,因此战术式方法甚至很可能无法让你的首个产品更早发布。
另一件要考虑的事情是,公司成功的最重要因素之一就是工程师的素质。降低开发成本的最佳方法是聘请优秀的工程师:聘用他们的成本不会比普通工程师高很多,但生产率却高得多。但是,最好的工程师非常重视良好的设计。如果你的代码库很糟糕,这个消息总是会传出去,让招聘变得更加困难。最终你可能只能聘请普通工程师。这将增加未来的成本,并可能导致系统结构进一步退化。
Facebook 在初创时期曾鼓励战术式编程。多年来,公司的座右铭是“快速行动,不怕犯错”。公司鼓励刚大学毕业的新工程师立即着手修改公司的代码库;工程师在工作的第一周就将代码部署到生产环境也很常见。从积极的一面来看,Facebook 以给予员工充分自主权而享有声誉。工程师拥有极大的自由度,几乎没有什么规则和限制束缚他们。
Facebook 作为一家公司已经取得了令人瞩目的成功,但是公司的战术式方法损害了代码库的质量。许多代码不稳定且难以理解,几乎没有注释或测试,使用起来很痛苦。随着时间的推移,该公司意识到这种文化不可持续。最终,Facebook 将座右铭改为“在坚实的基础架构上快速行动”,以鼓励工程师在良好的设计上进行更多投资。但 Facebook 是否能够成功清理多年来战术式编程积累的问题,还有待观察。
为了公平起见,我应该指出,Facebook 的代码可能并不比初创公司的平均水平差很多。战术式编程在初创公司中司空见惯,Facebook 只是一个特别明显的例子。
幸运的是,使用战略式方法也有可能在硅谷取得成功。Google 和 VMware 与 Facebook 差不多同时成长,但是这两家公司都采用了更具战略性的方法。两家公司都非常重视高质量的代码和良好的设计,也都开发了精良的产品,通过可靠的软件系统解决复杂的问题。这两家公司浓厚的技术文化在硅谷广为人知。在招聘顶级技术人才方面,很少有其他公司能与它们竞争。
这些例子表明,公司采用任何一种方法都有可能成功。但是,在一家关心软件设计并拥有整洁代码库的公司中工作会有趣得多。
3.5 结论
好的设计需要付出成本。你必须持续为它投入精力,小问题才不会累积成大问题。幸运的是,好的设计最终会收回成本,而且比你想象的要早。
始终如一地运用战略式方法,把投资看作今天而不是明天要做的事情,这一点至关重要。在紧要关头,人们很容易想把清理工作推迟到忙过这一阵之后。但是,这样很容易越拖越久(slippery slope):当前的忙碌过去之后,几乎肯定还会有下一次,接着又是下一次。一旦开始推迟设计改进,就很容易一直拖下去,让团队文化滑向战术式方法。设计问题拖得越久,就会变得越大;解决起来也越令人畏难,于是更容易继续拖延。最有效的方法是,每位工程师都持续为良好的设计做小额投资。
