想象把一个全新的软件产品当作独行的事业来做。首先,你得戴上产品经理的帽子,去理解你的客户、描述市场需求并开始定义你的产品。接着,你大概会轮流戴上架构师和用户体验设计师的帽子:识别支撑产品的核心概念、组织软件组件、设计相应的用户界面,等等。等设计推进到一定程度,你再切换到工程角色去写代码,然后切到测试做验证。在此过程中,你很可能还要分些精力处理项目管理的事务——即便只是管理你自己的时间。等产品发布后,还会分配一些时间到运维、客户反馈、市场与销售上。
要戴的帽子太多了——这也难怪很少有软件产品真的是单打独斗。随着团队扩张,角色会走向专业化。虽然没有精确的阈值,但当团队有几十人时,通常产品管理、项目/项目群管理、用户体验设计、架构、工程、测试、运维这些工作,都会分别由在各自岗位受过专门训练且有经验的个人承担。
因此,软件架构并不是孤立开展;它是众多专门角色中的一个,都是把产品愿景变成现实所必需的。此前我们在讲架构实践的内部运作时,多次提到要构建并维护架构与更广义产品组织之间的连接。现在我们再次把注意力放到:架构如何与软件产品开发中的其他专门职能协作、支撑并学习。
这些专门职能在不同组织里的角色、职责与称谓并不相同。这受多种因素影响很自然:组织规模、行业、公司文化,以及关于“如何最好地组织这些职能”的思想在持续演变。因此,本章采用的分类大概率与你所在组织并不完全匹配。如果你恰好在这些其他学科之一工作,你可能尤其清楚:你对自己工作的看法,未必与这里的描述完全一致。请注意:这里选用的结构主要是为讨论服务,并非要规定这些角色该如何命名,甚至不是要规定软件产品开发组织该如何搭建。
与开发方法协作(Working with Development Methodologies)
只要项目不是小到可以忽略,通常都会采用某种方法论来组织软件开发过程。方法论多如牛毛:敏捷、螺旋、RUP(Rational Unified Process) ,等等。这么多路径最能说明的一点是:没有一种方法能对所有组织、所有产品都“放之四海而皆准”。
方法论规定的是工作如何/何时开展,但并不会从根本上改变需要做什么。比如,再怎么选择方法论,也不可能不需要用户体验设计。方法可以规定这项设计何时进行、是一次性还是迭代式、如何排定优先级,等等。但“要做设计”这个事实不会改变。
同样地,不同方法论对于架构工作的何时做、怎么做看法各异,但没有哪一种能免除架构的必要性。因此,我们可以在不依赖特定方法论的情况下,讨论软件架构——它是什么、在怎样的上下文中运作、如何管理变更、如何做决策,等等。
此外,团队不可避免地会调整、改造、细化方法论以适配自身需要。方法论本身也在演变:新做法崛起,旧做法式微。若把架构实践绑死在某个方法论上,它会立刻变得不相干甚至过时。架构(就像用户体验设计)是一门每种方法论都必须纳入的学科,而不是反过来。
尽管如此,要建立有效的架构实践,你仍需让架构实践与产品的开发方法对齐——不管它是哪一种。可以把实践拆成两半来考虑:与方法无关的一半,和与方法相关的一半。
与方法无关的部分包括:架构原则、愿景文档与系统文档。无论采用何种方法,这些活动都应该开展,因为它们是架构工作的地基。如果它们恰好能与组织的方法论中的某些机制对齐,也很好。比如,很多组织都有年度规划的节奏,那正是更新愿景的绝佳时机。
与方法相关的部分是变更提案(change proposals) 。回忆一下:变更提案代表一段工作增量,把系统从当前状态带到未来状态。这个定义在任何方法下都不变;变化的是何时撰写变更提案,以及如何定界它们的范围。
例如,若某个方法强调先设计后实现,那其实就是在说:变更提案应该在设计阶段完成。因为是前期做完所有设计,你大概率会有相当多的提案要写,且范围可能更大。这段时间架构团队会非常忙,但对设计的需求并没有改变。
反过来,若方法强调即时(Just-in-Time)设计,每次迭代可能只拿起更小的变更提案。这种情况下,原则与愿景的价值会更加凸显——它们帮助在众多小步增量之间保持对齐。没有这些“强约束”,就有很大风险:许多小设计相互掣肘,而非相互强化。
以变更提案为中心的做法有个重要优点:可在上述两端之间伸缩。当开发方法发生变化或演进,你无需对架构流程做出本质性调整——架构工作终归还是架构工作。
这也有助于你在实践中灵活应对。设想你采用“前期设计”模式,却在实施中发现设计缺陷。你不可能把整个项目退回到设计阶段,但你可以补做少量的即时设计。灵活性有帮助。
反之,处理太多、太小的即时提案,可能比处理稍大、范围更自然的提案更难。因此,即便在高度迭代的方法中,某些变更提案也可能自然演进为更大的增量。
归根结底,架构流程应支撑组织的方法论,而不是主导它。同时,这些适配不应削弱团队把架构做好的能力。
与产品管理协作(Working with Product Management)
架构师从产品经理那里需要两样东西:
第一,一组描述接下来要做什么的能力(capabilities)与需求(requirements) ;
第二,这些能力在时间维度上的规划轨迹**。
能力是产品从客户视角能做的任何事情。能力常与特性/功能对应。比如,文字处理器有打印能力:触发后会选择打印机、为打印机排版、生成打印流、发送到打印机,等等。若你在做文字处理器,产品经理会希望纳入“打印”这个能力。
能力并不总是“功能”。它们也可能对应“非功能性”需求,通常涉及性能与其他可依赖性(dependability)指标。比如,你的文字处理器已经能打印,但只能打印不超过 100 页的文档。此时,把打印超大文档(如 10,000 页)描述成一个新能力是合理的。像这样“放大规模”的挑战,往往需要大量的架构工作。
能力应由一组需求来描述。就像架构产出的设计、规格与其他制品一样,需求也应当落到文档里——最好基于模板——以便捕捉细节并支持异步评审,而不仅仅依赖演示或讨论这类沟通形式。
需求阐明了一个能力必须做什么,以及(至少隐含地)不需要做什么。延续上例,“打印”能力的需求可以这样写:
- 应用**必须(MUST)**允许用户使用系统的打印设置与对话框,将当前文档打印到任何可用打印机。
- 应用**必须(MUST)**允许用户在打印文档时,为输出应用“DRAFT(草稿) ”水印。
仅这两条,对架构师就已提供不少设计信息。比如,很清楚我们不该投入做私有的打印对话框或连接——在某些应用里也许合理,但这里的需求清楚而有用地声明:我们的应用将复用操作系统提供的打印能力。毫无疑问,这是一条架构上重要的需求。
然而,这两条没有提到性能与规模相关的需求,这是疏漏。你可能以为这类需求“可以合理地被默认”——毕竟谁没用过打印机?——但在实践中,这种假设往往会惹祸。作为架构师,你也许觉得“每秒一页”很合理;而你的产品经理可能清楚,客户的高速打印机快一百倍。
作为架构师,你的任务不是去猜测这些缺失/隐含的需求,但你必须善于发现它们。经验丰富的架构师常能敏锐地捕捉这些空白,并要求产品经理补齐。在评审需求时,问自己:是否覆盖了吞吐、时延、规模、效率等?这些心智清单必然会因领域不同而有差异;想清楚对你的工作什么最重要,并努力识别空白。若可能,与产品管理团队把这些项补进他们的需求模板。
也要前瞻思考潜在变更。引入能力并不总是需要架构变更:有时现有设计能容纳它们(即便要写新代码);或者只需对设计做一些增强,仍然在当前架构边界之内。这些都是理想结果,说明当前架构与能力演进对齐,从而允许最低成本的实现。
当确实需要新工作,尤其可能需要架构性变更时,寻找能让你探索两种或更多方案的需求写法。产品经理有时会在需求里**“暗示”预期实现**;架构师有时也会在早期分享预想的变更方向而助长这种倾向。若大家看起来已经对齐,写出默认某个实现的需求似乎简单高效。
避免这种陷阱,对过分规定实现的需求提出异议。最明显的问题是:你可能改变主意。毕竟你之前只是初步想法;等看到完整需求并有更多时间思考,你很可能会选另一条路。而权衡多种备选本来就是工作的一部分。
更麻烦的是:若需求暗示 A 实现,而你选择采用 B 实现,你的实现就不符合“按字面写法”的需求——尽管它也许完全满足**“按意图”的需求**。字面与意图之间的鸿沟就是问题根源;避免它的唯一方法是写不带实现假设的需求。
更糟的是,这种鸿沟可能掩盖重大假设与误解。例如,产品经理写了“导出为 PDF”的需求。你最初的想法也许是复用打印能力:PDF表达的是页面的呈现;两者都需要版面布局。听到这点后,产品经理可能会被诱导把“导出为 PDF”要求成一种“打印机类型”。
但 PDF 不止是“纸上的墨水” :作为电子文档,它可以被加密、包含可填写表单字段,等等。这些是很棒但与打印页无关的特性,你的打印代码不会处理。那么,这是否意味着这些 PDF 特性不是需求?还是产品经理默认这些特性以后再加?
当需求规定“怎么做”而不是“要做什么”时,上述问题就会出现。若需求不给你在不同设计方案间选择的自由,它们很可能已经掉进了这个坑。即便你确实计划采用它“暗示”的实现,也要推动改写。在你把需求重新聚焦于“要实现的内容”时,可能会挖掘出新的需求或考量,从而影响设计。
在评审需求时,你还应当问:你(与产品经理)如何判断需求已被满足?理想情况下,需求应写成具体、可测试的断言。比如对“导出为 PDF”,比较这两种写法:
- 应用必须(MUST)允许用户将文档保存为 PDF。
这是可测试的——要么能,要么不能。但它不够具体,产品经理最终可能得到一个能力不足的版本。
作为架构师,你应尽早挖出隐含假设以便纳入设计。产品经理也许本意更接近:
- 应用必须(MUST)允许用户将文档保存为 PDF。
- 当保存为 PDF 时,应用必须(MUST)提供选项以密码加密文档。
- 当保存为 PDF 时,应用必须(MUST)提供选项将文档中的所有表单字段转换为可填写的 PDF 表单字段。
也可能并不需要这些附加项——只有简单版即可。无论哪种,必须说清楚。简单版(无加密、无可填写表单)更快、更省;不应为不需要的能力投入实现。相反,若确实需要这些附加特性,你的设计就要考虑这些选项。比如:需要把你文档中“表单字段”的概念映射到 PDF 的“表单字段”,这一点可能促使你重新思考实现方式,使两套实现在概念上更对齐。
Helping Out(协助支持)
我们已经详谈了架构对产品管理的诉求,但就像任何良性关系一样,信息应当双向流动。产品经理需要在客户请求、自身对产品/市场走向的判断、公司战略要求、进度与截止期等之间做一番复杂权衡。
作为架构师,你可以提供帮助。你可以指出哪些新特性/能力容易实现、哪些需要更大投入。尤其要让产品经理知道哪些能力具有较长前置期——例如因为需要大量设计工作或重大系统改动。前置期与工作量级是产品经理“算账”的两个关键输入。
更进一步的帮助是:与产品经理建立一套共同语言与对系统概念的一致理解。概念(第 2 章引入)提供了讨论“系统做什么”的理想抽象层级,不必陷入“如何做”的细节。围绕概念来讨论,等于在需求上实现了同样的分离:它保留了架构师在不破坏能力的前提下更换或重构设计的空间。
举例:产品经理想为应用新增“另存为 PDF”。他们意识到其与打印相似,想尽快上线。应用已经能打印,那么“另存为 PDF”肯定也能很快搞定吧?
概念能帮助我们抽丝剥茧,讨论这类问题背后的潜在假设。应用已有“打印”这个概念。若“另存为 PDF”被视为“打印”的一个侧面,那确实容易加入,但其能力也会受限于打印的范围——打印并不涉及加密与表单字段,基于打印能力实现的“另存为 PDF”也不会覆盖这些。
也许,产品经理会更愿意把它定义为一个新概念,例如“另存为 PDF”。该概念从一开始就纳入 PDF 特有能力;它会与“打印”相关,甚至复用一些代码,但并非同一概念。
或者,“另存为 PDF”只是格式转换这个更大概念的第一个特性。在此概念下,我们可以考虑输出 HTML 等其他格式;同时要抽象各格式的能力差异:PDF 可加密、HTML 不行,但两者都可包含可交互的表单字段。这个概念需要建模这些共性与差异。
要做到这一点,需将与产品管理的对话从“特性”转向“概念” 。一旦对概念有共同理解,产品经理就能更准确、且更独立地评估满足新需求的成本。这样也能把对话连接到架构所需的能力演进轨迹(第 3 章)上。
Other Outcomes(其他可能的结果)
并非每次与产品管理的协作都会导致系统变更。有时根本不需要;有些需求足够简单,可在现有设计边界内解决。若如此,把它记为一次成功,然后继续下一个挑战。
有时,在评审需求并把它们映射到概念、能力与特性的过程中,讨论会逐步延展,最终将该需求完全取消。这同样可以视为成功。
这种结果通常由两种情形产生。其一,评审暴露了需求不清。例如,原始需求里可能没有区分“打印为 PDF”与“另存为 PDF”。随后的讨论有助于厘清概念差异;要确定需要哪个,可能还得回看用例。
评审之后,产品经理也许判断:“打印为 PDF”即可满足用例;无需支持额外的 PDF 能力。这时还可能发现:操作系统自带的“打印到 PDF”就已足够。于是撤回该需求,不再投入工作——这是高度成功的结果,保住了宝贵的架构与工程资源。
其二,综合权衡后发现投入产出比过低。相反的例子:澄清后的需求表明“另存为 PDF”才是需要的能力;而实现它需要修改应用的文档模型——当前模型虽有表单字段,但无法映射到 PDF。虽可改,但改动范围从打印子系统扩展为对系统的广泛影响。产品管理可能据此合理地决定:不值得做。这依然是成功的结论——哪怕没有新特性上线。
Setting Boundaries(划定边界)
虽然偶尔会有“轻松解决”的新需求,但大多数需求都意味着需要做工作——无论是架构、设计还是实现。而产品管理负责排定能力的优先级与节奏——也就决定了相关工作的优先级与节奏。他们的核心职责,正是基于对客户与市场的理解做出这种取舍。
有些组织会把这一职责误解为:产品管理还能对所有架构/工程工作的优先与节奏拍板——也就是赋予产品管理权力,决定那些并非直接由需求驱动的工作何时进行。
这是应当避免的错误。当架构团队把这些决定拱手让给产品管理时,本质上是在放弃自己的职责——也就是判断这类工作是否应该做。这对产品管理也不公平,因为他们缺乏做此判断的条件。
这种行为出现,往往是功能失调的信号:架构团队想做某些架构/设计变更,却意识到其 ROI 低或尚不明确(常见于引入新技术的提案)。理性评估多半会否决。
把产品管理卷进来,也许是希望借其“背书”推动这类变更。但这仍然不公平:如果变更是为满足已有需求,那无需额外背书;若不是、而是纯架构性考量,产品管理如何判断是否应做?从产品管理视角,唯一合理答案常常是“不做”。
重点不在于“架构团队不能给工程工作排优先级”。相反,必须能。但当他们这么做时,必须准备好仅从架构理由出发为其充分论证。它应当是多个变更提案中的一个选择,为何取此舍彼应清清楚楚。
归根结底,引入新组件或复用既有组件、演化系统关系或维持现状——这些都是架构师应当做出的决定。不要因为畏惧决策而削弱架构角色。
这些决定可能极其艰难,但它们本就是工作内容。如果你在权衡上举棋不定,向同行请教或查阅其他资源(参见第 6 章的决策过程)。你可能会做出并不完美的决定——谁都会——但你仍应承担起责任。
与用户体验协作(Working with User Experience)
你在用户体验(User Experience, UX)方面的对应伙伴——有时也称为体验设计(Experience Design) ——是架构团队的关键合作者。尽管 UX 团队常以像素级精致的设计呈现他们的产出,但这些设计所代表的工作远不止界面。恰当完成的用户体验,应当反映并传达使系统“活起来”的那些概念。
当产品的用户体验准确映射其内在概念时,用户就能建立正确且有用的心智模型。有了准确的心智模型,用户会发现产品如其所料地运作:既能因贴合预期而令人愉悦,也能避免那种“摸不着头脑”的挫败感。
产品能否让用户满意,只有部分取决于它是否有用——这只是必要但不充分的条件。若用户误解了产品的工作方式,即便实现再精良,也难以上手。
因此,要做一个令人愉悦的产品,必须有能准确传达产品内在概念的用户体验。而要做到这一点,就要求UX 团队与架构团队在这些概念上达成一致。
在这里,人们可能会倾向于认为:由架构“理所当然”地确立系统概念,然后让 UX 团队准确传达这些概念。可这种态度不利于建立合作关系。
无论如何,一个无法向 UX 团队说明白、或本身说不通的概念模型是走不远的。如果 UX 团队都不理解它,你又凭什么认为用户能理解?
更好的做法,是与 UX 团队建立伙伴关系,共同对齐一个大家都认可、适用的概念模型。此讨论也应当纳入产品管理,因为一个好的概念模型应当通过“三重检验”:
- 满足产品管理所确立的需求;
- 能以合理的设计在系统架构中实现;
- 能通过直观明了的用户体验传达给用户。
当产品、体验与架构在这些核心概念上对齐时,“谁拥有概念”的争论也就无关紧要了。
与项目群/项目管理协作(Working with Program Management)
你的项目群/项目管理(Program Management)团队负责协调每个发布版本中的各项工作:分派、依赖、进度等等。当然,其中只有一小部分是架构性任务。
你可以通过澄清何时需要架构工作、何时不需要来帮助他们——这常常模糊不清。一次真正重大的产品变更可能需要架构变更;但更常见的是,只需在既有架构范围内做新设计;甚至有时无需设计,因为当前设计已能承载新特性。
这些区分对你也许一目了然,但对项目管理团队未必如此。尽早、明确地提供这方面的洞见,有助于他们把握工作量、范围与规模。
要让这类协作有效,项目管理需要理解架构团队可能采用的不同工作模式。如果你的团队此前没有遵循严谨的架构实践,花时间向项目管理解释这些区分与流程是值得的。
架构团队还可以通过制定、描述并让设计流程可见来帮助项目管理做协调,从而帮助项目按时推进。例如:
- 开放式工作时,项目经理可以轻松查看哪些设计已启动、哪些尚未开始;
- 使用标准化设计模板时,项目经理能迅速看出某个在制设计哪些章节已完成、哪些仍为空白;
- 明确负责人与审批人时,项目经理就知道找谁来确认状态与计划。
与项目管理的高效分工方式,是:通过完成架构工作的方法与机制实现职责分离——做什么由架构团队确定,何时做由项目管理决定。附带的好处是:这能让你和你的团队少陷入项目管理事务,把精力放在架构本身。
项目管理还必须关注工作项之间的依赖。在这里,你需要警惕不正确或过于严格的规则。例如,我多次见过:架构师不愿评审需求直到需求“写完”,工程师不愿评审设计直到设计“写完”。也就是说,他们暗含了**“完毕→再开始”(Finish-to-Start)**的依赖。
这种行为误解了工作方式。通常,更早的任务只有在它的消费者(需求的消费者是架构,变更提案的消费者是工程)认定其已完成时,才算完成。而消费者做出此判断的最佳方式,是他们已开始下一项任务!作为架构师,只有当我至少写出了完整的变更提案草稿,我才真的知道需求是否完整。同理,只有当工程进行了充分评审,我才知道我的变更提案是否完整。
因此,正确的依赖应为**“同步完成”(Finish-to-Finish) :后续任务不能在前序任务完成之前完成;但后续任务必须在前序任务完成之前就已启动**。
项目管理同样是规划与排期架构工作的资源。在大型复杂项目中,你需要同时处理不同范围与优先级的多项任务。若项目管理团队在向你索要具体日期承诺,那就找错了话题:你把过多排期职责揽在自己身上了。
相反,开放式工作:以你的流程为参照,列举需要完成的工作并估算其范围。接着,与项目管理共同制定计划,平衡并排定这些工作。你可能会发现:一个承诺 6 月完成的设计其实到8 月才需要,因为工程届时才有带宽;或者它8 月才开得了工,因为需求要那时才准备好。把对话从日期转向计划,就能让项目管理帮助发现、理顺并优化这些情况。
与工程协作(Working with Engineering)
再优雅的架构、再漂亮的设计,不落地就见不到天日——因此,你与工程团队的关系对成功至关重要。你应在每轮变更流程的前、中、后都与工程互动。
首先,以现有实现为基础。若你是新加入项目,先找到代码仓库并开始阅读。目标不是逐行读完,而是对实现的整体形状建立基本熟悉。
对代码结构与质量哪怕是基础级的熟悉,也能给你关键信息。比如,你在一个客户端中看到它调用各种 HTTP API。在以 Web 架构作为客户端-服务端通信的系统里,这不意外。也许架构团队设计了这些 API,或至少制定了 API 的设计标准。
即便如此,工程团队也会对调用的实现做出——有些甚至是关键性的——决定:他们用的是平台自带(操作系统)的 HTTP 库,还是自带实现?HTTP 库是多线程还是事件驱动?能否流式处理大请求/响应,还是把它们完整放在内存?
这些问题很多并无绝对对错。熟悉实现不等于对其吹毛求疵——虽然过程中难免会有评点。对许多应用来说,这些实现细节并不关键,多种路径都能奏效。
但有时它们就是关键。如果你的应用通过这些调用收发图片,而实现采用的是把请求/响应完整驻留内存的方式,那就不理想,并且无法扩展到更大的图片。若眼下不成问题,可以把它记下来,日后再看。
这类考虑常常当下无碍,但会因其他变更在未来暴露为问题。例如,当前对图片处理“还行”,但产品管理希望下个版本支持视频。支持视频会显著提高 API 负载的体量。
综上,熟悉当前系统是为了心中有数。通常,当前实现并不“错误” :它大概满足现有功能与性能要求——否则早就被当作缺陷处理了。
而且,要求每个实现一开始就能应对所有情形既不公平,也不必要。相反,选择更简单的实现以尽快交付首版往往更有价值;例如,流式 HTTP 库带来复杂性,可能会拖慢首发。没有先把“图片版本”发出去,恐怕你永远没机会再加“视频支持”。
你的目标,是对这些局限保持觉察并提前规划。评估新需求时,你应足够熟悉实现,知道哪些改起来顺手(只需小改),哪些会牵动更广、代价更高。
这份觉察是否该由工程承担?是的,让架构独自承担也不合理。另一方面,若架构总主观臆测代码实现,让工程反复纠偏也会令人厌烦。熟悉代码能让你与工程共享同一现实,在共同的基线上协作。
当你在撰写新的变更提案时,邀请工程参与。在开放式工作下,这会自然而然发生:你创建草稿,工程注意到并感兴趣就会来看。你可能会早于预期收到评论,但这种投入是积极信号。同时,开放式工作不是不做明确沟通的借口。若某个提案你确实需要工程参与,务必主动告知他们。
当工程参与评审时,项目管理可能还会让他们给出初步估算。估算与拆分是推动评审的绝佳视角:
- 一则它迫使工程全面梳理变更的各个方面,才能给出完整估算;
- 二则估算为工程提供“校验和”:若工程成本与架构基于现状的心里价位不符,就亮红灯,需深究差异。
这种差异有时源于沟通偏差。不管我们写得多清楚,工程可能不同解。如果是这样,直接对话能暴露误解——对齐并更新文档。
更深层的情况是:理解一致,但对工作量的估计仍相去甚远。此时要继续深挖——这常常意味着架构师对系统现状理解不够。也许变更方向正确,但更高的估算就得接受。
很多时候,更充分理解当前系统后,可以重新设计方案以降低实现成本。但这不是鼓励偷工减料!多数需求都有多种实现路径;我们在工作中持续权衡多项要素,成本当然是一项。我们不是要以牺牲功能/非功能指标为代价来“省钱”,而是寻找总体等效、但在当前系统下成本更低的备选。这也是为什么成熟的架构流程会在定调之前发展若干竞争性的概念方案的原因之一。
Following Through(跟进落实)
当变更提案获得批准时,你与工程团队的合作并未结束。对架构师而言,在整个实现阶段乃至产品进入使用/生产后持续参与至关重要。
若要不负“伙伴关系”的一端,架构团队就有责任与工程保持这种投入。随着工作推进,关于变更的疑问必然会出现,架构师应当在场并予以解答。别忘了,每一个问题也是一种反馈。这些问题往往出在变更细节不清或规定不足之处。应先解决当下问题,并在可能时把经验教训沉淀下来,为下一次迭代所用。
提到迭代,要避免在提案批准后直接修改该提案。需要澄清的是,后续仍可能需要变更:详细设计里可能有错误;在流程后期出现了更优方案也值得考虑;甚至需求被修订,从而需要重新审视原定变更。
无论变更动因是什么,都要记住:一切变更应通过变更流程来处理。也就是说,不要在已批准的提案上直接动手,而是新开一个提案来封装这组新的改动。正如第 4 章所述,坚持这一做法有助于保持团队对齐并减少反复。
在你跟踪实现的过程中,也可能识别出一些你想做的改动:它们对当前这组需求不是必须,但或许能简化系统、提升可依赖性、增加能力等。把这些想法记录进待办(backlog) ,即便它们未必都会实施,也不要让它们遗失。
Must Architects Write Code?(架构师必须写代码吗?)
在小团队里,角色分工不那么细,架构师也可能负责实现(至少实现部分自己提出的变更)。这没有问题——小团队对角色与职责的灵活性有天然要求。
在大团队里,人们有时会争论架构师“该不该/必须”写代码。潜台词是:不写代码就无法产出可信的设计,工程就不该听他们的。
这种说法站不住脚,也误解了专业分工的价值。我们并不要求架构师同时当产品经理,也不要求所有工程师同时当架构师,更不要求图形工程师还得精通 SQL 优化。没有理由把编码与架构可信度之间的联系看得比其他与角色错配的技能组合更加“真实”。
再者,架构与编码都是高度投入的智力活动。若要求任何人长期、持续地同时做好二者,本质上就是忽视把任一件事做好所需的努力。正因为它们既有区分又都不易,相关的专业化角色才会自然形成。
当然,良好运转的团队里,各角色成员对彼此带来的价值应尊重与欣赏。本着这样的精神,架构师与其他工程师(如同任意两种角色)需要建立彼此的信誉与信任。如果对某位架构师、某支团队来说,写一些代码有助于建立这种信誉,那就去做。但同样不要以为“会写代码”能替代作为团队协作所需的艰苦工作。
Working with Testing(与测试协作)
理想情况下,你的项目会有一支专门负责测试、验证、质量控制/质量保障的团队——不论它被叫作什么,其总体目标都是确保软件按预期运行。本文统称为“测试”,无意忽略不同称谓。
完善的测试职能会在多个层面验证产品。自终点往回看:测试会验证产品的能力与特性按预期工作——即不包含导致失败的缺陷。比如,调用某个 API,应当促使系统执行文档所述的动作,而不应产生其他动作、错误输出或内核崩溃。
完善的测试还会评估系统相对于可依赖性的要求,而不仅是基本功能。API 的规格不仅应描述它做什么,还要描述其扩展性(并发量) 、性能(响应时间) 、韧性(如面对硬件故障)等——这些都可被测试。
真正完善的测试甚至会验证:系统的能力是否满足作为设计输入提出的需求。换言之,我们不只想知道“API 是否做到文档所说”,还要验证“API 所做之事是否解决了它本应解决的需求”。
测试如何达成这些目标,超出了本文范围。就基本层面而言,测试可以对任何变更进行验证:是否行为正确、是否满足需求等。尽管如此,架构仍有很多方式可以帮助测试与验证。
先从文档说起——这往往也是测试团队的起点。从系统规格出发,他们可以识别可测试断言,据此制定测试计划与场景以进行验证。文档越好,这些测试本身越可能准确有效。
好的文档不仅描述系统做了什么,还要准确描述并传达系统所依托的概念,让读者建立正确的心智模型。测试人员有了正确的心智模型,才能正确推理系统应该与不应该做什么——这显然是构造完整、正确测试的基础。
写文档时,尽量从测试人员视角思考。好的文档总是为读者优化,而测试人员是你最关键的读者之一。读完文档,他们是否获得了足够信息去验证相应实现?
若能尽早让测试团队介入,就不必停留在理论上——邀请他们参与评审即可。更好的是,若你开放式工作,他们可以自发加入。尽早让测试参与,是更早开展测试、更早发现缺陷/误解(也更易修复)的极佳方式。
在系统设计时,也要考虑如何被测试。总体的设计挑战是:在保持系统完整性的前提下,尽量降低“黑盒”程度。系统内部状态越可见,测试就越容易验证行为;这种可观测性对调试也大有裨益。
多数情况下,你会发现这一目标与良好分解、低耦合的设计强相关。关键洞见是:当组件间的绑定被最小化,连接点就变成可检查点,测试即可围绕这些连接点编写。
测试团队可以多种方式利用这些连接点。最简单的是把它们用作检查接口:提供只读访问关键属性与状态的基础接口,都有助于测试在执行功能测试时验证系统状态。
检查中间状态如此有价值,以至于许多系统都会加入日志功能,等同提供一份持续开启、只读的“运行视图”。是否采用日志取决于系统类型;若合适,应把日志视为组件接口的一部分。虽不必像编程接口那样严格文档化日志,但记录基本预期(记录什么、何时记录、记录到哪里)能让测试团队更可靠地把日志事件用作验证的一环。
在更高级的方法中,可以利用连接点插入测试代码:它可以仅监控/记录事件与调用,也可以进一步改变组件间的行为。该技术可用于故障注入,从而合成失败状态并评估受影响组件的表现。
这些方法也与为单个组件或组件子集创建测试桩/测试夹具密切相关。组件通常期待与其他“真实”组件按标准职责集成;但若接口定义足够清晰且支持动态绑定,测试团队就能替换某些组件,以构造几乎任意的测试条件来验证其他组件。实际上,这几乎是唯一能测试一个组件是否足够健壮以抵御其依赖组件异常的办法。
上述技巧可以用于任何单项设计,但当它们被融入系统架构时会更强大,等于成为每个设计的标准组成。例如,若系统使用日志,就应在记录什么、何时记录、记录到何处上保持一致性。测试团队由此只需学习一次日志用法,就能在后续验证中一致运用。
同理,标准化系统元素之间的绑定方式也很有帮助。这也是许多架构会定义某种动态绑定机制的原因:在初始化期间,组件通过注册表/发现服务等查找依赖。统一机制既能简化架构,又能为测试人员提供唯一的控制点,以便插入监控、故障注入等逻辑。
最后,测试会产出大量信息,可用于指导下一轮设计。虽然这些数据在每个周期中较晚出现,但它们常会凸显系统特别棘手的方面。指标多种多样,缺陷数量或许最直观。主动索取这些信息,并据此识别系统中需要在未来设计中额外关注的部分。
与运维协作(Working with Operations)
软件产品并不会在测试完成时就“结束”。事实上,把测试视为“开始的结束”更合适。其后便是部署与运维。
与测试相似,部署与运维需求应被视作架构工作的输入。因此,架构团队应当在整个变更流程中与运维团队保持协作。
当然,并非所有变更都会影响部署与运维。通常,在概念阶段就能判断是否可能产生影响。此时你只是在决定如何实施该变更,细节尚未落定——这正是与部署和运维团队快速对齐的好时机。若无影响,他们就无需在此变更上投入更多时间;若有影响,则可在工作推进至详细阶段时持续参与。(并非所有组织都为这些职能设立了独立团队;若你们没有,请找到在各自团队中承担这些职责的个人。)
很少有系统自己定义一整套部署/运维机制。多数系统依托具备部署能力的平台:应用可能通过应用商店分发,云端服务可能使用容器编排系统,等等。更复杂的是,有些平台支持多种部署方式。移动与桌面操作系统既支持应用商店分发,也支持企业软件管控、侧载等。支持而非重造这些能力往往更明智。或者,你可能要向定制设备部署软件——除非你来构建,否则它们并不具备部署能力。又或者,你在数据中心环境中管理服务部署——选择虽多,仍需做出抉择。无论你的系统处于何种场景,部署与运维伙伴通常熟悉相关平台并能帮助你做出明智选择。
一旦完成部署,你还需要处理运维:监控运行中的软件、检测并恢复故障、变更配置,等等。针对不同设备、不同栈层,解决方案多种多样。
在演进架构时,你应就上述议题持续与运维团队协作。对此并无标准答案;而且和软件领域一贯的特点一样,相关技术更新极快。此处的目的并非给出“如何为部署与运维而设计”的建议,而是强调:这必然是联合投入。
即便如此,部署仍触及多数系统的一个关键架构问题:新版本很少能在同一时刻部署到所有设备。发布必然是渐进式的,在任何时刻都会共存多个版本。事实上,许多部署策略依赖逐步放量与在出现新问题时回滚的能力。因此,不仅新版本是分片式地部署,在许多系统中,旧版本也可能在新版本之后再次部署。
在某些受控环境中,可以对该问题施加一些约束。例如,你可以规定服务只能向前升级:若新版本有缺陷,就停止该轮发布并以更新的版本替换,而非回滚。不过,此选项并非普适。
一旦涉及客户端设备的软件部署,就无法避免旧版本安装或离线设备重新上线后仍运行过时版本。要做个有趣的实验:把笔记本关机几个月再开机,十有八九会经历数小时的更新,以追上操作系统与应用的积累更新。
此外,所有系统都会维护状态。若无状态,系统就没有记忆,也就毫无用处。状态形式多样:数据库中的行、文件、乃至仅持久化于内存快照等。
无论存放何处,状态都按某些规则持久化。通常,数据库对应模式(schema) ,文件对应格式(format) ;问题本质一致:持久化状态有其结构,而所有读写该状态的软件版本必须在该结构上达成一致,否则就会出错。
因此,有些架构必须容纳不同已部署版本对同一状态的读写。新版本需要能读取旧版本写入的状态——这显而易见。但除非你对部署有非同寻常的掌控力,你的设计也必须容纳旧版本读取新版本写入的数据。而让这种交错共存可行虽难,往往仍比**保证不存在任何“旧读新写”**来得容易。
你的部署与运维团队将站在第一线管理这些复杂场景。他们熟悉承载平台与你们系统的能力。当情况不妙时,他们也常常负责抉择:暂停或继续更新、回滚或坚持。基于这些原因,他们是架构团队不可或缺的伙伴。
小结(Summary)
架构只是开发、发布与运营软件产品所需的众多职能之一。要打造良好运转的产品开发组织,必须共同理解这些职能如何协同、彼此能提供什么。
组织通常会采用某种方法论(落实的到位程度各异)。这些方法论在生命周期组织方式上差异甚大,但它们描述的是如何组织工作,而非做哪些工作。因此,尽管架构团队必须适配不同方法论,制定原则与愿景、记录系统、以增量方式演进系统的核心工作并不改变。
与产品管理、用户体验、项目群/项目管理、工程、测试、运维建立并维护良好关系,是架构团队职责的一部分。这些职能为架构提供输入,架构也为它们提供输入。作为唯一负责理解整个系统如何运作的职能,架构在帮助各职能协同方面肩负重任。