卓有成效的软件架构——过程

50 阅读24分钟

架构发生在更广义的产品开发流程之中,该流程治理每个产品版本的生命周期。除去软件本身,软件行业还源源不断地产生各种这类流程:瀑布螺旋快速敏捷极限。这些流程规定了交付一个软件产品所需的步骤——收集用例与需求设计架构设计系统设计用户界面编程测试部署,等等。

这些流程体现了一个共同认识:必须管理变更。没有变更,我们就无法产出新的东西;但如果不去管理这些变更,几乎不可能得到一个可发布的产品。不协调的变更带来的不是软件,而是混乱

本章探讨高效的软件架构实践如何管理变更的流程。这些主题独立于具体的产品开发流程来呈现——之所以可行,是因为它们具有普适性,无论采用何种方法论都适用。一个有效的架构团队会在所在组织既定的结构与流程驱动变更。他们也许有偏好的流程,但不会声称自己的工作依赖于采用某个“正确”的流程。

管理变更之于架构实践,正如简单性之于架构本身:是未来成功的最强指标。当变更未被管理时,便会出现混乱——各项变更彼此掣肘而非相互配合。它们可能相互抵消,甚至把产品往回拉(例如降低可信赖性)。反之,强有力的变更管理能在可预期的时间内,配合任何开发流程,产出适用的结果。

为系统立档(Document the System)

变更从不在白纸上开始。与其说我们在做全新产品,不如说更多是在做产品的修订版。因此,多数架构师的大部分时间都会花在现有产品的下一版上。由此,描述系统当前的架构与设计就成为任何变更流程的第一步。如果我们不了解当下拥有什么,就无法对“要改什么”做出理性、知情的决策。

即便在做新产品,也请意识到:新产品并非在真空中出现。它可能基于旧产品的代码库;也可能计划复用既有的代码、库或设计;抑或借鉴了以往设计中的经验(宜为不宜为)。在这些情形下,理解既往系统的架构与理解现有产品上一版同样重要。

这听起来简单,做起来常常投入不小。项目往往更重视写出能跑的代码,而轻视描述代码结构与设计理由的配套文档。文档不是免费的;缺乏激励就不会产生。

而且文档还需要维护,进一步增加成本。面对重大变更(大功能,或需多团队协作的变更)时,不可能从头脑风暴直接跳到写代码。这会促使我们产出文档来协调与对齐相关人员。因而,变更越大,在启动阶段通常文档也越完整

然而,随着后续——尤其是小变更——不断积累,如果相应的文档、图表与制品没有更新,它们就会**“腐坏” 。当实现偏离早先的设想,文档的真实性会降低;迭代几次后,文档甚至可能误导**。此时读文档反倒妨碍工作,于是文档可能被彻底弃用。但放弃文档只会让问题更糟

不了解当前状态的前提下对系统动手,常以两种方式失败。第一种、也是最显而易见的,是提议的变更无法奏效,或所需投入远超预期。这往往要到实现阶段才暴露:系统开始失败,比如变更假定的某个不变式并不成立,或看似有效的输入其实无效。这类后期失败意味着需要回退重设,干扰性与成本都很高。

不必要的重复造轮子(Needless Re-creation)

不止一次,我见过一个新功能加进产品,后来团队才发现目标能力其实早已存在——也许以不同表述藏在某个大家不熟悉的系统角落。架构师也许知道相关功能存在,但由于缺乏深入调查,没能弄清如何复用它,而是另起炉灶做了个“相似却重复”的能力。

这种“不必要的再创造”更难察觉,也可能比实现阶段失败对项目伤害更大。首先是显见的浪费成本;最糟时,投入在重复功能上的每一分资源,本可用于真正的新价值。任何项目都有资源约束,这种浪费的机会成本很高。

它还会滋生复杂度——而复杂性是软件之敌:一件事两种做法,本可“一种足矣”。两个功能都要维护,却只需一个;后续每个改动都必须同时评估两套实现;客户也许要了解两种功能并耗费时间权衡用哪一个。这些成本会长期附着在项目与客户身上,除非再投入一轮把两套能力合并回去。

若文档已过时,团队就必须进行架构恢复(architectural recovery) ,以重建对当前系统的理解。根据现有文档(如果有)与系统现状之间的差距大小,可能需要多种手段:读代码、观察运行行为与数据;访谈一线同事,捕获其隐性知识并落笔成文。

当你着手架构恢复,必然会发现想要修改的现状设计——别浪费这些发现!但请把它们与恢复工作区分开来。恢复当前状态时,不能写你希望它是怎样;也不能边恢复边改,因为先还原现状实施变更的前提。请把这些拟议变更记入架构待办(backlog) (本章后文讨论)。

归根结底,准确的系统视图是评估任何变更必要前提。没有它,就无法准确评估提案。一个提案也许内在自洽(算法/模式等都正确),但如果与待变更系统不对齐,你就无法确定它能否奏效

一旦你获得了准确、最新的系统描述,请持续更新它。一个好办法是把文档更新纳入你的流程,且将其视为与代码同等重要的产出。正如我们能通过运行测试来验证实现,也可以通过阅读文档来验证文档。每次变更的完成,都会为下一次变更打下基础,周而复始。

面向愿景推进(Work Toward a Vision)

当你在构思对产品的变更时,你其实已有一个超越现状产品愿景。如果还没把它正式化(比如写下来),现在就是时候。

架构愿景应描述系统在三到五年时间框架内所期望达到的架构状态。正如第 3 章所述,任何成功且长寿的产品,其架构都会随着需求、市场与技术的变化而演进。架构愿景描绘了架构团队计划如何响应这些压力。

一个好的愿景应当具体而不过度细节化。例如,你的产品当前不支持扩展,但你看到了市场机会;那么愿景文档就应描述引入扩展支持,并覆盖主要考量:有哪些扩展点、扩展如何被发现、获取与安装。但它不应在此时去写扩展 API 的细节——那属于后续设计阶段的内容。

将愿景聚焦在三到五年有助于把握正确尺度。超过五年,你往往需要提出很多工作来“支撑”展望,内容就会过于臆测;同时多数市场里五年很久,超期的愿景更可能被打断而非兑现。而不到三年不够远,总盯着下一次改动,就无法对齐方向。愿景的价值不在于“对不对、妙不妙”,而在于它能对齐系统的并行动作。若方向错了,修订愿景重对齐即可;但没有愿景,各项变更同向推动相互拉扯的概率各半

同时注意控制愿景文档的长度:太短说不清,太长则细节埋没重点。建议约六页正文。若某些专题内容超出这一篇幅,可以用配套专题文档来扩展。例如,愿景提到把原在网站上的交易能力集成到应用内,那么可以新增一篇电商系统演进愿景的专题文档。

架构愿景与团队的架构原则一道,是团队最重要的产出之一。首次编写往往需要可观投入:安排时间收集干系人意见调研相关技术与市场趋势;留出时间让团队提出多种选项并展开辩论;让工程与产品等干系人审阅草稿并反馈。完成后,尽可能广泛发布

发布愿景后,请规划定期复审。不同于“每次变更都更新系统文档或架构”,更新愿景不需要随改随更。在三到五年的框架下,每年一次刷新通常合适:一年里你会对愿景有实质进展,可以删去已完成项,并把展望再往前推一年

很多时候,年度更新可能变化不大——这是好事。再强调一次:愿景的意义在于对齐方向,而方向不该频繁摇摆。当然,遇到市场或技术的扰动,你可能需要做更大的修订;此时,一次重写愿景文档也能向所有人发出信号变化发生了

撰写变更提案(Write Change Proposals)

就像把系统的当前状态设想的未来状态写下来很有价值一样,为了从当前走向未来,我们计划实施的变更也应当以变更提案的形式书面化

一份完整的变更提案应覆盖变更的三个阶段:其动机概念性方案、以及详细设计。但撰写变更提案的意义不是一上来就把所有信息都理顺;相反,变更提案充当一个容器,用于在提案演进过程中逐步收集这些信息。

变更提案可以从一两句开始:说明为什么需要变更(动机)或可能要改什么(概念)。在提案的早期版本中,应聚焦大图景:这次变更能否满足新的需求?它与我们的系统愿景如何对齐?

早期提案也可以描述要改动的对象,例如哪些系统组件或关系会被演进。但不必(也不应)过早细化实现方式先对齐动机与概念性方案更为重要。

变更提案是捕获、讨论、打磨拟议改动的核心机制,因此一开始不必精雕细琢。如果你心中已有具体改动,当然可以在提案里说明——例如建议为系统新增一条架构原则,这已相当具体。但要点是:任何拟议变更都应被记录,不论是否“架构级”。在初期,变更范围未必明确:你对“新增原则”的提议,后来也可能演化为修改既有原则

有些变更提案会随着时间推进到详细设计阶段。设计是对某个事物(功能、算法、服务)如何工作具体且细致的说明。凡是通过概念评审的变更提案,都会进入设计阶段(第 5 章详述设计流程)。

举例:你被要求让系统(存储文本记录)支持基于文本的搜索。对应的变更提案可以是:启用并利用数据库的内建文本搜索能力。在概念层面通过后,仍需继续敲定详细设计。在概念阶段,提案只是圈定将要修改的组件集合

并非所有变更提案都需要详细设计。例如,某个提案可能建议增补愿景愿景既不是系统的架构,也不是设计,而是对未来方向的陈述。对愿景的更新也是变更:需要修改愿景文档,并通过你的变更流程来管理;但它本身不需要设计工作。

我们往往把变更与增量添加划等号,因为产品开发强调构建新功能。但变更≠必然新增:变更也可以是删除功能修改既有功能(更快/更省/更可扩)、或调整系统的其他方面

元变更(Meta-Changes)

不同团队的形式化程度各异;有些团队会严格文档化他们的变更流程——也就是他们管理变更的实践。那么,如何修改“变更流程”本身?当然也要通过变更提案

对一些人来说,这类“元变更”显得有点过头;但也有人乐于见到可自我更新的流程。无论如何,依我之见,统一适用的变更流程具有去中心化/赋权效应:任何人都可以提出任何变更,每个变更都能得到公正对待。当然,这并不意味着所有变更都会被采纳——绝非如此

维护待办(Maintain a Backlog)

如第 3 章所述,对系统的每次变更都会经历三个逻辑阶段:识别动机制定概念方案落实详细设计。尽管实际流转未必线性,你的流程仍应显式跟踪每个变更穿越这三阶段的状态。这样,即使从中途开始,也不会在未对齐动机时推进概念方案,或在未对齐方案时直接做详细设计。

当前/历史/未来变更提案的清单,加上它们各自的当前阶段,共同构成你的架构待办(architectural backlog) 。维护待办的概念与敏捷实践联系紧密,但迭代/增量开发的思想以及“需要跟踪下一步可能做什么”的需求,早在敏捷之前数十年就已存在[5]。这里有意与敏捷术语对齐:假定你的组织某种程度上采用敏捷,那么“维护待办”的概念就易于理解与传播

不要混淆架构待办 ≠ 产品待办。前者包含变更提案,描述架构性工作;后者描述功能/能力。两者相关但非一一对应。比如,产品待办中的一项可能对应多个变更提案(因为需要评估备选方案,或因为涉及多项变更);反之,产品待办中的另一项可能没有对应的变更提案(并非每个新功能都需要架构工作)。

如前节所述,处在动机/概念阶段的提案文档可以很简短(几段文字)。因此在这些阶段,待办条目本身即可充当其文档。但进入详细设计阶段后,你需要更完整的文档。第 5 章专门讨论设计阶段,第 7 章则对管理架构待办提供进一步指引。

考虑备选方案(Consider Alternatives)

如果在概念阶段只产生了单一方案,那么进入该变更的详细设计往往会很顺畅。缺少备选意味着无需辩论,甚至可能不需要正式拍板才能推进。对于以小步迭代为特征的开发流程而言,这种结果合理且常见——这类流程天然要求对系统架构进行更小、更渐进的更新。做小变更时,遵循现有架构更可能只指向一条路径

尽管如此,概念阶段仍是产生并比较备选的最佳时机。延续前文示例,除了使用数据库的内建文本搜索能力,还可以提出另一份提案:给系统增加一个独立的搜索引擎来满足同一搜索需求。动机相同,但概念方案不同;两者都能满足需求,但成本与性能特征不同。为多份提案留出空间,有助于发散思考,从而形成替代性概念

image.png

图 4.1示意了提案之间的关系:多个概念可对应同一动机的不同路径,而同一概念也可能衍生出多个详细设计。其中多数会被淘汰,但这应被视为健康的变更流程,而非失败。

早期探索备选方案还能在计划与执行层面提高可预测性。若架构师直接推进第一个想法,一开始似乎进展神速,但往往在较晚阶段暴露问题。每个变更都有代价面,区别只是早发现还是晚发现晚发现将导致高成本纠偏;有时甚至需要完全换一种概念方案

当问题暴露得晚,是否还要转向探索备选概念本身就会压力重重:一方面,人们会因既有投入偏误而更黏着当前路径;另一方面,潜在备选尚未充分展开,看起来容易**“想象得更美好” ——就像起初的路径当年看起来也很美好一样。此时,大量时间精力会在如何继续的决策上消耗,而时钟在走**。

若存在备选方案,不要把它们揉进一份提案里。沿用示例,“要么用现有数据库,要么上独立搜索引擎”的单一提案并无助益——那只是把动机换了个说法。

要求每个备选都作为独立的变更提案提交、评审与评估,有助于结构化讨论。将其在架构待办中分别维护,也能留下一份清晰的“考虑过但否决了哪些备选”记录。相比“一份历史曲折的长提案”,你会得到一组简洁提案及其各自的结论。

这也强调:提出变更并非走过场。显然,有些提案会通过概念阶段(否则就不会有进展),而另一些——可能很多——会被否决。被否决并不代表提案有缺陷;它也许只是在某个相关指标上不如另一个备选。

成熟的架构实践产出多份概念提案否决相当一部分。当做到这一点,说明两种关键行为在发生:

  1. 团队具备创造力,能产出多种可比路径。第一个想法未必最佳;即便最佳,也不能孤立判断。做出一组备选往往带来更全面理解更优结果
  2. 它鼓励广泛贡献。哪怕个别架构师很高产,我们每个人都有偏好与成见。通过鼓励更多备选将“被否决”常态化,团队为不同观点与点子提供了空间。这些提案未必被选中;关键是避免把筛选过程变成竞争。甚至评估未采纳提案的过程,也常能强化最终入选的方案。

若面对一项重大变更自然涌现不出发散路径,请主动催生备选。若直接向团队要备选没反应,可以先自己抛几个。它们不必优秀,甚至不优秀更好:我常故意提出自己不会接受的替代,只为激发更好的点子——很少失望。这个阶段的方案往往也会是相互变体组合等。

保持有序同样重要:在待办中,把相关提案彼此关联,并关联到它们所对应的需求。在做概念选型时,决策者应能随手取用这些信息。

当然,选项太多也会形成负担。经验法则:即使对重大变更,超过四种左右的相关概念方案通常就过量了。同时,保持每个概念提案的工作量轻量化,可避免这一流程阶段不必要的沉重

当你手里有两个或更多竞品提案,迟早要选其一。若仅凭提案材料仍无法立即决断,可以并行推进多于一个方案的详细设计,并在之后只选一个实施。但应尽量减少进入详细设计阶段的提案数量,因为每个都需要不小投入。不要让“多方案并进”成为回避决策的拐杖。(第 5 章会详细讨论决策。)

择优时,还应评估单一提案是否能覆盖更广的需求集合。有时这很明显,比如多个相关需求显然需要公共解。更大的收益来自:识别看似分散的需求,能否由同一底层机制满足。评估提案时,把更广泛的架构考量、你的愿景与相关需求纳入分析,可显著助益。

最后,别在概念阶段逗留过久。若提议直白无备选,应尽快前进;若备选复杂需要更多时间评估,也不要让决策无限拖延

不做之事(Not Doing Things)

在“做软件并交付”的语境里,人们容易把“成功的变更”等同于实现→发布→被客户使用。这常常成立,但并非总是如此。

强健的变更流程会在多个方面逼出清晰度,包括但不限于:

  • 要解决的问题:最初的工作也许基于不清晰/不完整/误述的需求。
  • 实现成本:每次变更都是一笔投资;并非所有投资都有好回报。在撰写提案的过程中,可能会发现成本将超过任何合理收益
  • 运维成本:云服务会带来计算与存储的持续成本;嵌入式系统的变更可能影响硬件 BOM(例如需要更快的 CPU)。

基于这些或其他原因,对某个提案或一组相关提案的评估结果可能是全部否决

被否决的提案不等于失败。恰恰相反,最好在概念阶段就识别出这些问题,而不是到详细设计/实现/发布之后越早做出这样的决定,浪费越少避免组织陷入昂贵的死胡同,是架构团队能提供的最有价值的贡献之一。强健的变更流程不仅告诉我们如何去做,也告诉我们哪些不该做

紧急与重要(Urgent versus Important)

盲目套用一种架构设计流程,它将难以高效;其应用应考虑具体情境

当工作确实紧急时,要求在推进设计流程前完整的系统文档写齐、把愿景文档更新好,是不合理的。安全、合规/法律等紧迫问题可能带来不可回避、不可协商的情形与截止期。一旦出现紧急工作,团队必须就手头已有的信息尽力而为。

事实上,唯一能为紧急工作做准备的时机,是它出现之前。有些团队把恢复并维护架构文档看作“税”,但它更像保险费:确实要花钱、也不确定何时用得上,但迟早会遇到意外。严谨的系统文档是为突发事件做准备的最佳投资之一

如果你发现很难挤出时间做有纪律的变更流程(它会产出并维护准确的系统文档),不妨意识到:你的大多数工作是重要的,但很少是紧急的。为某个任意定下的发布日期去交付新功能是重要,但未必紧急。为非紧急的工作走捷径会削弱你的应急准备。真正重要且不紧急的工作,最值得认真、彻底地去做。

至于既不重要也不紧急的工作?别做它——把时间用来把重要的事做好。

重新记录系统(Redocument the System)

管理变更的最后一步把我们带回开端:我们必须把变更后的系统记录下来。只有这样,闭环才算完成,也才为下一次变更做好准备。

所需投入自然取决于变更范围。推进提案的过程中,你也许收窄了范围,也许无需架构层面的改动;这时可能只需要小幅更新某一份文档。

当然,影响更广的变更需要更多工作。尽管不应让单个提案无限长大,但即便是聚焦的改动也可能带来更广的更新。例如,新增或修改一条架构原则,不仅要改原则文档,还可能波及标准架构说明、甚至引用该原则的设计文档

更新文档时,你可能会识别出更多应当进行或至少应当考虑的改动——把它们登记为新的变更提案并加入待办

最后,记得告知相关方这些更新。沟通将在第 8 章更详细讨论。

将变更提案视作 Pull Request(Change Proposals as Pull Requests)

当下的软件开发实践中,Pull Request 的用法与这里描述的变更流程大体类比

  1. 代码为准,先有系统基线的描述;
  2. 创建拟议变更,可被视为对基线的diff
  3. 把拟议变更作为 Pull Request 分享,用它来收集反馈并继续完善
  4. 若获批准,则**合并(merge)**该 PR,从而更新代码。

变更提案“合并”进当前系统文档的过程,仍远比合并代码 PR 更为手工——也许未来会改变。眼下,这个类比依然有助于解释变更提案流程。

总结(Summary)

变更是软件开发的核心。但变更不应是无结构、无纪律的活动。高效的软件架构实践借助变更流程来推动:让架构团队探索备选减少回溯与重复,并聚焦最重要的工作

图 4.2 展示了架构变更流程:它从当前系统描述开始(若缺,则需从实现中恢复出来);同时需要一个面向未来的愿景,并由系统所处的上下文加以校准。

image.png

图 4.2 变更流程概要。上下文、愿景、恢复与文档等活动与工件,为流程核心的三阶段提供信息支撑;待办用于跟踪当前、潜在与历史变更。

流程的核心是变更提案:它们承载变更从动机 → 概念 → 细化的推进。团队应使用架构待办跟踪过去、现在与未来的变更提案。某个提案首次从待办中被拉出时,通常处于动机或概念阶段;也可能是此前更完整的提案回存后,此刻被再次拾起继续推进。

随着提案演进,可能会提出备选,进而生成并评估动机、概念或细化层面不同的新提案。其中一些会并行深化——同时,任何时候提案都可以放回待办以待后议。

当某个提案被批准实施时,必须更新系统文档以反映新的系统状态;这份最新的文档将成为后续工作准确基线

在整个过程中,要牢记:并非所有变更都会完成。有些会因不值得而放弃,有些会因行不通而中止。强健的变更流程会尽可能识别这些情形,而不是执着于“把每个变更都做出来”。

最后,架构师应当在实现与部署推进时持续关注这些提案。此阶段的经验教训虽已来不及反映进已批准的提案,但完全可以、也应该影响未来工作——例如形成新的变更提案,或在上下文与愿景中得到体现,从而继续引导后续的变更流程。