Philosophy of Software Design 第三章 仅仅能工作的代码是不够的(编程时的战略 vs 战术思维)

好的系统设计最重要的元素之一是完成编程任务时所采取的思考方式。许多组织鼓励使用战术性思维,专注于尽快使功能上线工作。然而,如果想获得好设计,则必须采取战略思维,在简洁的设计和修复问题上投入更多时间。这章讨论了为什么采用战略思维可以获得更好的设计,而且从长远来看,实际上要比战术思维更节省时间。

3.1 战术式编程

大多数程序员采用我称为战术式编程的方式进行软件开发。这种方式中,主要关注点是得到能工作的东西,比如一个新功能或修复一个 bug。第一眼看去,似乎完全合理:还有什么能比写出可以工作的代码更重要呢?然而,战术式编程很明显不能产生一个好的系统设计。

战术式编程的问题在于短视。采用战术式思维编程时,你就会尽快地完成一项任务。可能面临着硬性规定的截止时间。最终,为未来做计划的优先级就会变低。你不会在寻找最好的设计上花费太多时间;你只是想要尽快获得可以工作的代码。你会自我催眠,认为如果可以更快地完成当前任务,那么增加一点复杂度或者混入一两个不和谐因素也没什么问题。

这就是系统如何变复杂的。正如前一章中讨论的那样,复杂性是增量的。并不是某一个特定的问题,而是数十或数百个小问题的积累使得系统变复杂了。如果你采用战术式编程,每一个编程任务都会向这些复杂性做贡献一些问题。每一个问题可能看上去都是为了快速完成当前任务的合理折中。然而,复杂性会迅速积累,尤其是如果每个人都采用战术式编程的话。

不久之后,一些复杂性就会开始导致问题,你将会开始希望当时没有走捷径。但是,你仍会告诉自己,使得下一个功能尽快工作要比反过头来重构已有代码重要得多。长远来看重构可能可以帮得上忙,但是绝对会拖慢当前任务的进度。所以,对于碰到的任何问题,你就会寻找可以解决它的快速补丁,而这又会在将来需要更多的补丁。很快代码就会变成一团糟,但是此时想要清理代码的话将耗费数月的工作。你的日程不可能负担得起这样的延迟,而且修复其中一两个问题看上去也不会有什么效果,所以你会仍旧保持战术式编程。

如果你曾经在一个大型软件项目中工作过很久,我猜你曾经在工作中见过战术式编程并且经历过它带来的问题。一旦你开始采用这种方式,就很难做出改变。

几乎每个软件开发公司都至少有一名把战术式编程发挥到极致的开发人员:战术式飓风。战术式飓风是一名高产的程序员,他地代码产出速度比其他人快得多,但是完全以战术式思维的方式工作。当涉及实现一个快速功能时,没有人可以比战术式飓风更快的完成。在一些公司中,管理层将战术式飓风视为英雄。然而,战术式飓风会留下一系列破坏痕迹。他们很少被将来必须和他写的代码打交道的工程师视为英雄。通常,其他工程师必须清理战术式飓风造成的混乱,这会使得这些工程师(真正的英雄)看上去比战术式飓风的进度更慢。

3.2 战略式编程

成为一个好的系统设计师的第一步是要意识到 仅仅能工作的代码是不够的。 为了完成当前任务而引入不必要的复杂性是不可接受的。更重要的是系统的长期结构。任何系统中的大多数代码都是在已有代码的基础上扩展而来的,所以作为开发者最重要的工作是为那些将来的扩展提供便利。因此,不应当把“可以工作的代码”当作首要目标,尽管你的代码当然必须可以工作。你的首要目标必须是获得好设计,然后也恰好可以工作。这就是 战略式编程

战略式编程要求有投资的思维。相较于采取最快的方式完成当前项目,你必须投资一些时间提升系统的设计。这些投资短期看会稍微拖慢你的进度,但是长期来看它们会加速你的开发,如图 3.1 所示。

一些投资是主动的。比如,为每个新建的类多花一点时间找到简单的设计是值得的;与其实现蹦到脑子里的第一个主意,不如尝试一些其他的设计并选择其中最简洁的。试着想象一下未来系统可能会向哪些方向改变,并且保证你的设计可以使得这些改变会很容易。编写良好的文档是主动投资的另外一个例子。

其他投资将会是被动的。不管你前期投资了多少,你的设计决定不可避免地会出现错误。随着时间流逝,这些错误会变明显。当你发现了一个设计问题,不要忽略它或仅仅通过打补丁解决;花费一些额外的时间来修复它。如果采用战略式编程,你将会对系统设计持续性地做出小改善。这是战术式编程的反面,那种方式下你会持续性地增加复杂性并在将来引发问题。

3.3 投资多少时间?

所以,投资的时间正确数量是多少呢?巨大的前期投资,比如尝试设计整个系统,是低效的。这是瀑布式方法,而且我们知道它不奏效。理想的设计倾向于随着你获得系统的经验而一点点地出现。因此,最好的方式是在连续的基础上作出大量的投资。我建议花费大约占开发时间的 10-20% 在前期投资上。这个数量足够小,不会显著影响你的日程安排,但是又足够大,可以随着时间获得明显的收益。因此你最初的项目花费的时间要比纯粹的战术式方式长 10-20%。这个额外的时间会导致更好的软件设计,并且几个月内你就会开始享受这些好处。不久之后,你的开发速度就会比当时以战术式编程的人快至少 10-20% 。到这个时候,你的投资就会变成免费的了:从你过去的投资中的获益将会节省足够多的时间覆盖将来的投资。你将会快速地从初始的投资中恢复。图 3.1 展示了这种现象。

图 3.1

图 3.1:开始,战术式编程会比战略式编程进度更快。然而,战术方式下复杂性会更快地积累。随着时间流逝,战略方式进度会更快。注意:这张图只是定性的说明;我不知道任何可以对这个曲线进行精确测量的经验

相反,如果你采用战术式编程,你将会更快地完成第一个项目,但是随着时间流逝,你的开发速度会随着复杂性累积而变慢。你将会很快地将开始节省的时间还回去,而且在这个系统剩下的生命中,你的开发速度会越来越慢。如果你从未在糟糕的代码基础上工作过,和其他有过这种经历的人聊一聊;他们会告诉你糟糕的代码质量至少会拖慢 20% 的开发速度。

3.4 初创企业和时间投资

在一些环境中有着强大的力量反对战略式方式。比如,早期的初创企业有着将他们的早期版本发布出去的巨大压力。在这些公司中,看上去即使是 10-20% 的投资也支付不起。最终,许多初创企业采取了战术方式,在设计上花费的时间很少,清理出现的问题时花费的时间甚至更少。他们用这样的想法将这些行为合理化:如果他们成功了,他们会有足够的钱雇佣更多的工程师来做清理。

如果你在有这种倾向的公司中,你应当已经意识到,一旦代码库变混乱,想要修复基本是不可能的。在这个产品的生命中你可能需要支付很高的研发花费。而且,好(或坏)设计的报应来得非常快,所以,战术方式很可能甚至无法加速你第一个产品的发布。

另外一件需要考虑的事情是,公司成功的最关键的因素之一是它的工程师的质量。降低开发花销的最好的方式是雇佣厉害的工程师:他们的成本不比平庸的工程师多多少,但是他们有着高得多的产出。然而,最好的工程师非常关注好的设计。如果你的代码库一团糟,事情会传出去,你会更难进行招聘。最终,你很可能只能拥有平庸的工程师。这回增加未来的花费,而且很可能会导致系统结构进一步降级。

Facebook 就是一个鼓励战术编程的初创企业的例子。很多年来这家公司的座右铭是“快速行动并打破东西。”刚刚从大学毕业的新入职工程师被鼓励立即深入公司的代码库;工程师在他们入职的第一周就向线上提交代码曾经是很常见的。积极的一面是,Facebook 作为一家给员工赋权的公司而闻名。工程师有着巨大的自由,基本没有什么规则和约束阻碍他们。

Facebook 作为一家公司曾经非常成功,但是它的代码库由于公司的战术方式而遭受了损失;大多数代码都不稳定而且难以理解,基本没有注释和测试,使用时非常痛苦。随着时间流逝,这家公司意识到它的文化是不可持续的。最终,Facebook 把座右铭变为“在坚实的基础架构上快速行动”来鼓励它的工程师在好的设计上投资更多时间。Facebook 是否能成功地清理数年来战术式编程累积地问题还有待观察。

对 Facebook 讲句公道话,我应当指出 Facebook 的代码可能不比初创企业的平均水平差多少。初创企业中战术式编程是家常便饭;Facebook 只是恰好是一个特别明显的例子。

幸运的是,采用战略方式也可能在硅谷中成功。Google 和 VMware 差不多和 Facebook 同时起家,但是这两家公司都拥抱了更战略化的方式。它们都很重视代码的质量和好的设计,而且都基于可信赖的软件系统构建了解决复杂问题的精致的产品。这些公司强烈的工程文化在硅谷变得出名。很少能有别的公司可以在招聘顶级人才中竞争得过它们。

这些例子表明公司可以以任一种方式成功。然而,在关注软件设计并拥有干净代码库的公司工作要有趣得多。

3.5 结论

好的设计不是免费的。你必须持续性的进行投资,这样小的问题就不会积累成大问题。幸运的是,好设计最终会偿付它自己,而且比你认为的要更快。

采用战略方式时从一而终是非常重要的,而且要把投资当作今天要做的事情,而不是明天。当你进入一个紧张的工期,把清理工作推到这个工期结束后会非常具有诱惑性。然而,这是一个滑坡;当前紧张的工期结束后,几乎总是会有另外一个,然后又是另外一个。一旦你开始推迟设计提升,推迟很可能会变为永久的,然后你的文化会滑向战术方式。你等待解决设计问题越久,问题就会变得越大;解决方案会变得越吓人,这又会使得推迟解决变得更容易接受。最有效的方式是每个工程师都为好的设计持续性地做出投资。