经营一套软件架构实践、打造一款产品、管理一支团队、推进一次变更、修复一个缺陷——这些活动都涉及无穷无尽的决策。归根结底,我们交付的产品,正是我们选择要做的变更与选择不做的变更的累积。因此,强大的决策能力是高效软件架构实践流程的关键组成部分。
我们越高效、越有效地做出这些决策,交付就会越好、越快。与其让每次决策都以临场即兴的方式发生,不如建立结构化、可复用的流程。这样还能锻炼我们的决策能力:让到达结论的过程更可预期、更迅速,并减少结果欠佳或需要重审的决策数量。
逐个看决策时,我们往往关注输入与输出。输入是我们手头的事实或观点,是我们权衡选项时可变动的变量。输出是决策的结果——例如能否产出更好的产品、是否能加速交付进度,等等。从这个视角看,每个决策都是独一无二的。
换个视角,我们也可以关注各个决策之间不变的部分。具体的输入/输出总在变化,但每个决策都有输入与输出,都涉及做决策的人(以及批准者、贡献者或被告知的人),并都遵循某个流程与时间线。
谈到决策流程,人们常常会想采用(或自创)一种刚性强、结构严密、文档完整的框架。对于少数影响广、后果重的关键决策,重量级流程没有问题——但这种决策并不多。大多数决策都较小:其影响往往来自累计效应,而不是单次的成败。
因此,发展决策能力的真正挑战在于:如何让决策方式缩放到团队中每个人每天所做的几十个小决策。面对大量小决策,若流程要求详尽文档、跟踪、会议、通知,反而弊大于利,也不会被遵循。一个“缩小版”的决策流程应当只需要:意识到此刻在做决策,并提供易于遵循的指引。
本章主体就是一组用于此目的的提问。团队与个人可以针对每个大小决策快速自问这些问题;它们的答案会帮助判断何时应推进决策,以及哪些决策需要更多时间与关注。
更多信息会有帮助吗?(Will More Information Help?)
没有相对完整的信息,就做不出好决策;但也有大量决策会因为徒劳地搜集无关信息或无影响的信息而被拖延。因此要问的问题不是“我们是否掌握了全部信息? ”,而是“我们掌握的信息是否足够? ”
显式地问这个问题会有帮助。你也许已经向多方收集了信息,但很难确定他们是否把全貌都告诉你了。也许他们对你的问题做了狭义解读;也许他们没有补充更多信息,因为你没问、显得麻烦、或他们不确定其相关性;也许只是一时间想不到。
如果你直接问还需要知道什么,就为贡献者创造了补充分享的空间——他们原本可能不便主动追加。你也可能因此“震落”一些此前没想起来的信息。无论他们提供了什么,都应该表示感谢——即便对这次决策未必有用,你将来还可能需要他们再次主动分享,负向反应会抑制这种积极性。
若仍不确定信息是否足够,就问问自己:若再多掌握哪些信息,会让你改变决策或带来更高把握?然后检查你是否已有这些信息,或者需要去补齐。
然后就停止继续收集。既已与贡献者与自己都确认过,接下来就做决定。此后再去“广撒网”地搜集信息,失去聚焦,大概率收效甚微。
例如,设想你的系统里有一条“使用(uses) ”依赖:服务 A 依赖 服务 B 执行某项工作,即 A 向 B 发起请求/消息。你近期意识到这条依赖值得商榷:
- 服务 A 只调用服务 B 的一个操作,称为
F()。 - 在服务 B 维护
F()代价高,因为需要 A 团队 与 B 团队 协调。
于是有人提出一个变更提案:通过把 F() 从服务 B 挪到服务 A,来移除 A→B 的“uses”依赖。要做的决策是:是否推进。
基于已给信息,还有一些与决策密切相关的补充信息你会想了解,例如:
- 这次变更是否会在 服务 A 与其他服务 之间引入新的依赖?若
F()自洽,可能不会;但若F()自身还有依赖,就要看它依赖的是哪些服务,以及 A 是否已依赖它们。 - 任何新增/移除的依赖,是否符合/违反系统架构中关于关系的约束?
还有些信息可以收集,但对决策并不关键:F() 最初为何在 B 里?过去 F() 是否用于他处?未来是否可能有更多服务调用 F()?将来会不会又在 A→B 间创建新的依赖?这些问题会带来更多信息,但并无助于决策;反而可能把无关且带猜测的信息引入讨论,搅浑水。
做决策时,努力做到信息恰到好处、不多不少。足够的信息让你做对且少反悔;不过量的信息让决策过程持续推进。
在此期间发生了什么?(What’s Happening in the Meantime?)
决策需要时间——也许不多,但从不可能瞬时完成。我们可能出于多种原因拖延:也许这事看着不起眼,不觉得有紧迫性;也许我们仍在搜集信息;也许我们难以抉择。
与此同时,现状持续。作为决策者,我们必须意识到这一点,并将其纳入考量。若该决策只影响系统一个孤立、无人动工的局部,你大概率不急。在此期间发生的事既不会影响你的决策,也不会被你的决策影响。
但设想你仍在斟酌是否将 F() 从 B 挪到 A。与此同时,还有几个团队正在开发依赖 F() 的改动。因为 F() 当前在 B,这些团队就会实现并集成对 B 的依赖。
你的决策时机会严重影响那边的工作:
- 若你尽快决定把
F()挪到 A,会给其他工作带来一些影响,但越早决定,影响就越小。 - 若你最终决定挪动,但在此之前犹豫了更久,就会制造更多工作:在你“下定决心”的这段时间里,其他团队已经把他们的改动集成到了 B。此时再挪动
F()就更贵,因为F()的调用方更多了。 - 若你放弃这次变更,其他团队就可以按原计划继续,无需新增工作;但 A 与 B 团队必须继续协同。
记住:你在做决策时,系统也在同时变动。若你的决策会影响或受影响于这些并行工作,就要把这一点算进去。你越快决策,该决策被“地面事实”扰动的可能性就越小,期间发生不幸的概率也越低。
眼下有几道决策在进行?(How Many Decisions Are in Play?)
决策并不总是以最简单的形式出现。看似一个决策,实则可能更适合拆成多个。例如,生产系统发现数据丢失缺陷,显然需要一个如何修复的决策。但这可能需要时间;与此同时,数据丢失风险仍在。在这种情况下,如何临时消除风险(例如下线某功能)与如何永久修复(修缺陷)可以视为两个独立决策。
反之,看似两个决策,实则可能只有一个。回到第 5 章的例子:两个服务团队都在考虑如何为其存储的记录加入全文检索。表面上看是两个团队各自的决策;更好的视角是:这是一个关于系统所有记录如何支持全文检索的统一决策。把它当作一个决策,就为建设统一的搜索能力留出空间,让该能力为两个依赖服务同时提供支持。
与此同时,“是否提供统一搜索能力”与“为该能力选择哪种技术”又是两道决策。若不知道至少有一个可行实现,你不可能去决定采用统一搜索;但若存在多个实现选项,把选哪一个也捆绑到同一个决策中,会让流程超载变慢。在此期间,团队可能各自先上了自己的全文检索,这恰恰是我们不想要的结果。
架构师因为具备系统级视角,很适合帮助厘清应当做出多少个决策,以及它们之间的依赖关系。同时,这种“看大局”的倾向也容易让人把多个决策捏在一起。一支有效的软件架构团队会避免此类陷阱,而是去识别并做出一系列离散的决策,并在推进过程中清晰、一致地沟通每项决策的数量与范围。
不做这件事的成本是什么?(What’s the Cost of Not Doing It?)
评估一次变更时,本质是在维持现状与实施变更之间做选择。一方面,实施变更的成本在其他条件相同下相对容易估算;另一方面,维持现状常被误以为是“零成本”。毕竟,什么都不改,怎么会有成本呢?
当然,几乎所有系统都有持续成本。即便不再追加新投资,也需要维护。安全漏洞可能需要响应。在云环境里,计算、网络、存储的费用也在持续发生。
还记得那位工程师写的、用一种小众新语言实现的组件吗?他/她已经离职了,现在团队里没人会这门语言。大多数时候它能“自动运行”,但偶尔需要修补或升级。每次动它都很贵,因为每回都得有人现学一点点这门语言来修问题——我们自以为修对了?——然后提交代码,努力把这茬忘掉。
如今有人提议重写这个组件,改用我们偏好的编程语言。重写看上去很贵,因为它把成本一次性集中发生;而维持这个组件的持续成本则被切碎并分散在时间里,难以核算。人类直觉会回避一次性大开销;把一次性成本与持续成本做可比的权衡,本就很难——这是直觉的盲点。
如果把这些持续成本加总起来,你很可能会发现重写反而划算。没错,它有一笔前置成本;但做完就做完了。此后该组件仍可能偶尔出问题,但修复它的成本与系统里其他部分相当。同时,去掉一门额外语言带来的简化还会持续受益。
需要明确的是,提出这个问题并不是鼓励我们比现在做更多变更。并非每次变更都会降低系统的维护或运营成本——这通常也不是大多数变更的目标。重点在于,人们往往过分关注眼前成本而忽视持续成本。自问持续成本,能帮助我们校正这种倾向。
我能接受这个变更吗?(Can I Live with This Change?)
很多时候,我们不是在决定要不要变更,而是在决定做哪种变更。这类决策常涉及速度与质量的权衡,其中“质量”是对方案多方面属性的主观评价。比如,往系统里加一个特例可能被视为“较低质量”,而引入一个更普适的新行为则被视为“更高质量”。
此时,人们容易走向一种折中:先做快但低质的方案,以后再补昂贵但更好的方案。显而易见的问题是,这等于承诺团队要做比两者其一更多的总工作量。我从未见过一个团队愿意把一次就能做好的事做两遍。
当然,折中的初衷在于:短期能把事情尽快做出来(“快”的部分),长期仍承诺一个高质量实现(“慢但更好”的部分)。理论上两边都满意。但现实中行不通。
原因在于,团队无法把自己长期绑定到未来的工作上。产品团队总会面对新的资源需求——持续不断的新功能、新技术、缺陷修复、优化项。任何“昂贵但更好”的未来承诺都会长期与这些工作竞争资源。多数时候,那些工作确实更重要——尤其当“第一步”的低质方案已经落地时。
这并不意味着你不能选择“快但低质”。意味着的是:你必须愿意长期与之共处。如果你选了它,就要接受可能再也没有机会回头重做——你会一直用它。若你能接受这一点,那你当初可能也并不需要那个“昂贵但更好”的版本。
架构团队往往觉得这个问题格外难,也许因为“昂贵但更好”的选项更契合他们对高质量的追求:更优雅、更创新、更有成就感。这会让架构师加倍坚持昂贵选项,甚至拒绝协助低质但更快的方案。领导层可能会试图用承诺“以后再做”来“安抚”——但这类承诺难以兑现。
为避免此局面,要强调:架构师也要与决策结果长期共处。虽然放弃“更好更贵”的方案会失望,但当下的机会是:把“快但低质”打磨到可用——也许会稍贵一些,但够用。换言之,架构师应当寻找**“适度昂贵、可长期共处”的方案**,位于两个极端之间。一支有效的架构团队,会在资源与时间约束内做出大家都能接受、且不破坏系统完整性的决策。
技术债(Technical Debt)
把低质量变更称为**“引入技术债” ,而后续的高质量变更则是“还债” 。团队可以记录自己“负债”的多少。理论上,技术债是对“以后修”承诺的记账机制**。
但把它类比为“金融债务”往往误导。当你背上“好债务”(如小企业贷款),实际上是在做会产生回报的投资——未来的收入会用来偿还这笔债。你在购置设备、招人,这些都在提升企业价值。
技术债恰好相反。背上技术债,并不是为了投资;而是在累计你因低质量实现而少投了多少。投资会产出回报,而糟糕实现带来持续开销:维护、运营、故障、缺陷。这会让“还债”变得更难,不是更容易。
若一定要用债务类比,技术债更像是你负担不起却刷卡去的度假。有些人最后能把信用卡债还清;但也有不少人选择破产。
选错的代价是多少?(What Is the Cost of Getting This Wrong?)
面对一个决策,我们常以为必须得到正确答案,甚至假设只有一个“唯一正确”。现实很少如此。
结果当然有好坏之分,但很多时候它们只是不同。这在命名(属性、类、服务等)讨论中常见。命名很重要,但它不是只有一个解的方程。会误导的名字当然不好;但当两个名字难分优劣时,我们往往拥有多个合理选择。若“正确”的那个与“错误”的那个同样好,那就不存在代价。
“对错”框架的第二个问题是忽略了结果的寿命。确实,有些决策难以更改,比如主要编程语言——通常一旦选定就会长期共处。
但多数决策范围更小,且发生在持续演化的系统里。若一个决策可以或将会被重审,我们就应该在上面投入更少时间。把大量时间花在一个仅维持几周或容易更改的决策上,并不划算,这些时间/精力应当用在更难改的决策上。
意识到这一点后,可以把“对错”换一种问法:选错的后果是什么?如果改起来很便宜,那尽快拍板、必要时再纠偏也许更省。事实上,在不确定性高、改动成本低的情形下,先向一个方向走,即便以后改主意,可能仍然比“先做出更好决策”更便宜。
一支有效的架构团队,会评估选错的成本,不会在易于修正的决策上过度投入,而是把时间和精力留给那些改起来代价昂贵的决策。
我还能把把握提高到什么程度?(How Much More Certain Can I Be?)
当你把决策清单走到这一步时,可能仍然无法百分之百确定结果。也许你还有时间做决定,也不担心在此期间会发生什么。你已经收集了大量信息,但仍不具定论。桌上有两三个选项,都不完美,而且无论选哪个,你都得长期与之共处。这种情形确实存在,也会让决策深陷泥潭。
此时,你必须问自己:还能获得更多确定性吗?更多的时间、信息、备选项或分析会有帮助吗?通常,这些活动都体现边际收益递减:在决策流程的较早阶段,你就能得到大部分确定性。把过程拉长也许有点帮助,但除了最琐碎的问题之外,你永远无法达到100% 确定。
事实是:决策总是在信息不完全、确定性不足的情况下做出的。你也许以为自己掌握了全部信息,但更可能是仍有未知的未知。在明知仍有不确定、可能出错、未来也许要重审的情况下做决定,会让人不舒服。我们最好的应对,不是追求绝对确定,而是承认其不可达。没人喜欢这样,但如果决定是你的职责,你就需要承担风险,做出决定,然后继续前进。
当然,这不是为草率、仓促、信息不足的决策开脱。有时等待确实能带来新信息;给决策留出思考也可能带来一开始没有的清晰。如果你对一个决定明显不确定,你应该争取更多的时间、信息或分析来改变这种状态。带着不确定前进并不等于草率判断,而是认识到“绝对确定永不可得” 。
这是我该做的决定吗?(Is This My Decision to Make?)
到目前为止,本章都在讨论任何人都能用来引导决策过程的问题——它们聚焦于阻碍个人做决定的因素。不管怎样,这些障碍都需要被处理,然后做出决定。
但在决定之前,你还必须问:这个决定该不该由我来做?理想情况下,职责边界和清晰的岗位描述会给出明确答案。若你真遇到这样的岗位,请告诉我——我也想去应聘。
作为架构师,标准、原则以及其他架构层面的决策通常是我们的职责,也应当由我们做出。我们会接收一些输入(如需求),它们会约束我们的决策,但如何满足这些需求由我们决定。
然而,有些决策的影响超出架构范畴。例如,我们可能希望使用第三方服务或软件;这往往会引出合同与费用问题。这并不意味着不能考虑此类方案,但意味着需要把不同的决策者拉进来。
当面对具有更广泛影响、涉及更多利益相关方的决策时,可以考虑采用正式的决策框架。一方面,如前所述,这类框架对小决策来说开销过大;另一方面,它们施加的结构正是为了处理复杂权衡与多元观点下的复杂决策。
例如,有些组织会使用某种职责分配矩阵来识别参与者及其角色。若某决策由你拍板,你就是“批准者/对结果负责者(Accountable/Approver) ”。若你在推进一个决策,但别人才是批准者,你就是“建议者/驱动者(Recommender/Driver) ”。根据具体方法,矩阵中还可能包括被咨询、需知会、需评审、需提供输入、需签字等角色。
当然,有些决策需要上推,另一些则应下放。如果你对某个子系统负有架构责任,这个决定能否交给该子系统下属的某个服务/库的负责人?如果你负责一个库,这个决定能否交给具体负责类或函数的工程师?找到这些授权机会有两点好处:
- 腾出你的时间。你大概率很忙,有必须由你完成的设计和决策。能下放的一律是从你的待办上拿掉,为其他工作腾挪空间。
- 锻炼他人。也许该服务负责人还比较资浅;给 TA 机会评估并拍板,就是一次学习。授权时别只是一推了之:提供背景、解释为何交给 TA、表示可随时指导,然后真正让 TA 负责。
这两种模式——向上升级与向下授权——是相通的。今天你也许把问题上推给高层;明天一位工程师可能把问题上推给你(作为架构师)。那些习惯把决策放到合适层级的团队,最终会做出更优决策。
我是否保持一致?(Am I Aligned?)
有些决策是“大决策”:涉及众多利益相关方,会影响成百上千人的工作,能拉动或拖累产品收入,会让公司走向长期投资或合作。它们的重要性源于影响范围,也因此广受关注——哪怕大家对是否“做得好”看法不一。
软件开发过程蕴含数不清的决策:从架构原则、子系统边界,到函数命名、for 循环不变量……工程的本质就是通过这些决定把产品具体化。
显然,大多数不是“大决策”。没有人需要为“for 还是 do-while”去召集干系人、写长文档。但对影响重大的决策,我们应当投入更审慎、更多样的决策过程。做决策的投入应与其影响相匹配。一个在小决策上耗时太久的团队,会动作迟缓,而与此同时,别的事在发生。
本章这些问题被设计成能跨尺度使用:对最小的决策,这些问题可以即刻回答、无需征询他人;若无法快速回答,这就提示它影响更大,需要更认真对待。
然而,大量的小决策虽然快速且分散地做出,合起来也会产生巨大影响。这是必然的:它们位于“大决策”与具体实现(算法、代码、数据结构)之间。
鉴于此现实,每一个决策——再小也不例外——都应与架构原则和技术愿景保持一致。正是这种一致性保证了成千上万的局部决策在整体上协同推进产品目标,而不是相互掣肘或背离目标。在做出任何决定前,都要自问:它是否与产品的架构原则与愿景对齐?
我能把它文档化吗?(Can I Document It?)
根据我的经验,没有什么比尝试把一个决定写进文档更能带来清晰。不止一次,我在脑中觉得很清楚的事,一动笔就原形毕露。如果你无法清晰地记录一个决定,这强烈说明你还没有做出清楚一致的决定。
事实上,在作出决定之前就考虑如何把它写进文档,会促使你提前回答许多必须解决的疑虑与问题。尽早起草那份文档,正好给你一个随着推进梳理思路的载体。
话虽如此,我不建议滥用“决策文档” ——也就是只为记录决定本身而存在的文档。能不单列就不单列:尽量把决定写进更大的工件里,比如变更提案、架构规范、设计文档、愿景论文,甚至是代码注释。把独立的决策文档留给那些确实无法与上述材料打包的少数场景。
这条建议主要出于实操考虑。团队每天要做不计其数的决定,大小不一。给每个决定都配一份单独文档显然不现实。如果已有的工件(代码或文档)已经涵盖了上下文、决策内容与相关方,那就没必要再造一个独立的决策文档,价值也不大。
举个例子,第 5 章把变更提案引入为架构团队提出、评估并拍板变更的机制。变更提案不仅仅是一个决策记录,它还会记录提案本身、相关背景、干系人、考量因素等。但它同样可以记录决策:最终每个提案要么通过,要么否决。这时另写一个决策文档也加不出新信息。如果你把架构工作围绕第 4 章的变更提案来组织,并遵循第 7 章的实践,你几乎很少、甚至用不着单独的决策文档。
架构决策记录(ADRs)
有些团队会使用架构决策记录(ADR) [8],并以一组 ADR 作为系统的架构文档。ADRs 与第 4 章的变更提案有些相似,但侧重点不同:
- ADR强调记录已经作出的决定;
- 变更提案强调对潜在变更的探索与试验。
如果团队只依赖 ADR,就相当于用作者的便利来交换读者的便利:写 ADR 的人只需把决定记下;而读者要理解系统架构,就得把所有 ADR 都翻一遍。更糟的是,后来的决定可能覆盖早期的决定,而每位读者都得自己把这些覆盖关系捋清。这就像你想读源码,却不得不逐条阅读代码仓的每一次提交,而不是直接读已应用所有提交后的当前版本。
文档应始终为读者优化——读者远比作者多得多。若你的团队使用 ADR,请务必在每次决定之后同步更新系统规范。就像变更提案一样,当没有人再需要回头查 ADR时,你就知道做对了。
小结(Summary)
决策无处不在,不仅存在于架构之中。我们在工作中往往专注于事实,但就像任何可复用流程一样,关注过程本身能让我们做得更好。善于决策是一种独立技能,可以被培养与提升。
确实会有少数重大决策值得采用比本文更正式的流程:它们需要单独跟踪、文档化并正式批准。不过,大多数决策既不需要、也拿不到这种规格。本文提供的一组问题可以快速套用到每一个决策上——牢记它们。