前言

本文是《架构随笔》系列的第五篇,也是它的收官之作。

架构的定义

架构是一个界定不清的东西,我们很难讲清楚哪些东西是架构,哪些东西不是架构。但软件行业里其实人人都在搞架构,软件设计就是架构本身

架构这个词出现得很早,有些人认为是 NASA(也可能是NATO) 发明的。最早的架构定义就是描述软件的结构而已,但现在已经没有多少人谈论他们定义的“软件架构”了。工程师很难以克制描述复杂结构的原始冲动,但描述复杂结构的普世标准并不存在。大家常见的各种定义,翻来覆去地重新讲着“软件架构是软件结构的顶层设计或者抽象设计”之类的话。即使是这种软件架构的定义,也并不为所有人都接受。汗牛充栋的架构书籍里有各种各样的观点,有的进一步把软件架构视作一堆组件和交互的设计,有的则把软件架构视作架构师主观意图的体现。把自己当作架构师的人们,着迷于把软件里的“不变与抽象的部分”和“易变与具体的部分”分离出来,把前者当作架构。架构师们是如此地热衷于做这样一件事,以至于有些人认为架构设计好了就解决了基本问题,设计不好通常是因为架构不好。于是很多人开始刻舟求剑:从某某颗粒度开始的设计应该叫概要设计,从某某颗粒度开始的设计应该叫详细设计,寻求一个稳定、确切的合理软件架构成为了设计一个软件的先决条件。为了防止架构出错,架构师们的原始冲动会让他们寻求叠床架屋的解,详细地从问题域推导解空间,然后一层一层堆流程。架构的设计俨然和建筑学里面的蓝图(blueprint)一样,需要一开始就设计得严丝合缝。于是我们在软件工程课本的早期历史部分能见到了瀑布流方法论的失败、OS 360制造了人月神话(Dijkstra 把苏联决定建造 OS360 兼容机称作“美国在冷战中取得的最大胜利”)。硅谷无数的“死亡行军”、“梦断代码”项目为这种流程制造反动,又产生了敏捷运动,SOA 的颗粒度又被进一步缩小,产生了 MSA。如果你在上世纪七十年代参与一项软件工程的架构设计,你要写的架构文档可能长达50页,包含各种角度的架构描述;如果你在上世纪九十年代参与这样的项目,你要写的文档可能会缩减至20页(这要部分感谢软件分工的进步,操作系统、工具链、中间件和业务逻辑终于被分离开来);如果你在当代参与一个中国互联网公司电商系统的项目设计,可能有的架构师会按照4+1视图构建一些架构基线,有的架构师会写一些兼容 TOGAF 的视图,有的架构师干脆只提供矩形、棱形组成的图(没错,它应该是一幅流程图。但大部分人并不遵守 UML 里流程图的规范来画图)。在不同的架构师的视角里,架构到底应该详细到什么程度、应该包含什么要素是无标准可循的

这就产生了一个问题:如果我们不能搞清楚到底什么是架构而什么不是架构,我们只能把“如何评价一个架构的好坏”这个问题,转换为“如何评价一个系统的好坏”。我们无法区分系统中宏观和微观的优缺点,那么,“一个系统的架构会决定基本面貌的好坏”这个假定听起来就不那么准确了。虽然它仍然符合人类的直觉,但我们无法决定,什么时候应该改进我们的“架构设计”,什么时候只是改进我们的某些“非架构设计”。除非我们肯承认,我们应该改变的不是架构设计,而是设计本身

只要稍加观察,所有人都会同意,人类天然就有架构能力。人类在现实世界里理解各种各样的架构,也创建各种各样的架构。在软件领域之外,人类设计了多样的复杂结构,用来表达世界的复杂,也用来消解管理世界的复杂的心智负担。我们有房子的架构,有组织的架构,有业务的架构,也有软件的架构。所有工程师在第一堂软件工程课里都在不断学习:如何控制复杂度。老师们会不厌其烦地教大家,如何拆类,如何拆子类,如何拆函数,如何拆子函数,又如何把这些程序组件连起来,成为一个可以运行的程序。不管这个程序的组件叫模块还是叫例程,不管这个软件的结构是不是丑陋的,它总归是有结构的、且合乎一定道理的。这说明人类有一种把复杂的东西拆成 segmentation 的天然能力和欲望。人总要把用各种各样的载体把问题装进一个封闭空间里,然后面向封闭空间求解,导出问题空间的数与形,然后映射成解空间的要素,再组合得到一个心安理得的解。这种 segmentation 的能力就是架构的能力本身,每个人天生就有这些能力,每个工程师在初级工作里也大量使用这种能力。从这个角度看,架构能力既非架构师的专利, 架构也就不只是是架构师的造物,架构并非单单是抽象设计,架构就是软件设计本身。当然,这个观点并不新鲜,也并不是我发明的。我最近一次听到这种观点,是在21年 QCon 大会上陶文的演讲里。

从外部视角来看,还有其他的定义可用,这些定义更加不拘泥传统的标准。近些年像《整洁架构》、《软件方法》之类的书里就引入了一些从结果定义架构的思路:软件架构是对业务用例的回应,软件架构通过对软件复杂度的控制,为利益相关者(stakeholder)负责

架构是为了解无解的问题-分工

架构如何回应业务用例?如何为利益相关者负责?

所有的架构都是为了解决分工问题。正如组织架构是为了解决不同职能的人的分工问题一样,业务架构是为了不同职能的业务模块和流程的分工问题,技术架构是为了解决技术组件的分工问题。这种分工对架构有决定性的的影响,绝大部分的技术架构都无法逃脱康威定律的支配。商业组织的软件开发者大多对这种现象司空见惯。在商业定律的支配之下,软件开发只是生产要素起作用的一个基本环节而已。软件架构对业务用例的回应方式是:让各种业务用例用恰当的模块分工落地。软件架构对利益相关者负责的方式是:让利益相关者在付出最小的使用和维护成本的前提下,得到最高的生产力,创造最多的核心价值

分工是很难解的永恒难题,实际上并无真正的可行解。因为复杂和混乱是无法彻底消除的,而且任何组织自身的发展过程里,总不可避免出现低熵体逐渐演化为高熵体的情况。王兴有一个很有意思的洞察,商业和战争很像,一方面讲创造,一方面讲竞争。可以说,这种战争是永无止境的,创造是永无止境的,熵增也就是永无止境的。兵法里讲,兵无常势、水无常形。永远会有新的业务模式出现,也永远会新的组织模式出现,商业组织里很难有稳定的架构,激烈变动的组织里连相对稳定的架构都很难寻求-很多研发者也可以把这句话解读为,活是永远干不完的。这产生了这样的常态:再好的架构,也难以避免两类问题。从一方面看,因为资源的原因,任何系统一开始就不足以让人心满意足,软件架构师们总是想着一个 System V1 还有种种缺点,不停歇地构思出一个 System V2,最后失败(这类故事在《人月神话》很多);从另一方面看,任何系统永远要防止从一个“还行的系统”跌落到“屎山”的状态,架构师在战火连天的商业周期里提心吊胆,生怕复杂度失去控制。架构如兵法,以正合,以奇胜。

大部分互联网公司的新工程师,都会指责老架构不堪大用。他们转化为老工程师以后,又会感慨老架构牵一发动全身:很多问题不上称只有二两重,上了称一千斤都打不住。因为商业战争的快速变化,在一套系统里,各种野生架构和推定架构(这个词我们下面会再次谈到)并存,不同技术生代的造物被各种飞线黑魔法连在一起。有些工程师看着后端架构里,Velocity 写的页面和 React 写的页面混搭,就好像看着石器时代的尖矛利盾和火箭时代的火控雷达,被火车的挂钩连起来一样。这时候有些心存幻想的工程师也许会无能狂怒:到底是不是存在我还没有学会的架构理念来处理这个问题,是不是当时我理解这个问题不够深刻?

后面他们会有心无力地发现:没有银弹。架构是不能完全解决分工问题的,且它仍然要在这个支配性的枷锁下实现一个个业务用例,它仍然要为利益相关者负责。在此前提下,我们应该接受一个事实:架构应该是一个抱残守缺的东西,我们只能在不好里寻找好。ycombinator 社区里有条评论很应景:“There’s no silver bullet, only trade offs.I see this a lot and it bugs me, because it implies that it’s all zero sum and there’s nothing that’s ever unconditionally better or worse than anything else. ”

抱残守缺的好架构应该是怎样的

如何应对业务复杂度

软件是用来解决现实问题的,所以软件首先是对现实的抽象刻画

在数学里,解是输入的映射:y = f(x)。y 是 x 的产物,y 不同于 x,重点是找到 f。

在工程领域里所有的解都存在于解空间之中,而解空间又必须对问题空间负责,所以是从问题空间导出的。现实空间中有非常非常多的要素,要素之间有各种各样的关系和生命周期。哪些是应该进入我们的解空间的要素,哪些要素应该经过转化和重新抽象,哪些要素是我们凭空创造的(例如,现实中并不存在时间与事件,我们如何在一个系统里表达时间与事件?),这都是需要我们深入思考的问题。有些架构师们喜欢把思考这种过程的能力称作解构和抽象的能力。一个架构要能够满足业务用例的诉求,其基石就是解空间里从现实空间中正确刻画出来的元素和交互。《人月神话》把软件的复杂度分为必然(实际上应该翻译作主要)复杂度和偶然(实际上应该翻译作次要)复杂度。作者认为,基本要素的复杂度是不受工程师控制的,也就无法减少,而把它们组合起来的系统的复杂度却受很多创造过程里的路径方法和组合的影响,是受偶然因素影响的。对问题基本要素不恰当的理解,很可能造成不恰当的必然复杂度,也就不可能得到多么低的偶然复杂度。

这个观点看起来是容易理解的,但工程实践里却很难被做到。大部分的业务系统的构建者往往远离第一线的现实场景,通过产品经理来理解现实问题。而对技术团队的现实问题输入,却又凌乱且无序,甚至很多时候并不完整。快速迭代的商业节奏和流水一样的职场更替,让业务研发团队对问题空间的理解很容易变得支离破碎。到底领域里有几个边界、几个模型,每个模型的主子关系如何,模型有多少种状态,状态的跃迁关系应该是怎样的,一起决定了一套系统设计的方方面面。这绝不是工程师能够拍脑袋决定的(但是现实中经工程师却经常被迫做“那我来拍一个”式的决定,仿佛设计一个系统好像买一个西瓜一样简单),必须认真听取利益相关者相关的输入。如果只是得到一些一鳞半爪的输入,架构就无法贴合业务的发展,在走形中腐化,在腐化中走形。

可能有鉴于此,这些年来软件开发领域里,领域驱动设计的思潮开始流行起来。大家终于逐渐认识到了领域知识才是设计的驱动方,这个主次关系不可调转。要获得对真正的复杂业务的认知,要尊重领域专家的意见。很多时候因为种种资源的原因,一个技术团队找不到领域专家,只有请类似领域的领域专家来主导业务设计。譬如请电商的产品经理来当金融的产品经理,把金融票证的设计当成电商商品来处理,只要业务的场景稍加变化,整套架构就会显得非常生搬硬套,令维护者如鲠在喉。有时候连类似领域的领域专家都找不到,架构者只好自己冒充领域专家,自己同时扮演主次关系的双方闭门造车,架构的好坏听天。模型的错误、交互的错误、关系的错误都是只重视框架而不重视细节的架构设计常犯的错误,而且很容易在项目后期导致项目失败(这再次说明了系统设计里任何细节都可能是架构设计的一部分)。兔子有四条腿,桌子也有四条腿,如果构建一个买卖兔子的系统,能否满足制造桌子的需求呢?很多架构上似是而非的错误,需要到很晚的时期才能被发现,而且绝没有后悔药可吃。对一个领域有完整的认知或者有构建这种认知的能力,是成为一个领域里合格的架构师的先决条件。一线研发团队成员平时工作节奏非常紧张,学习领域知识被看做是一件肤浅的脏活累活,很多人都没有办法构建这种完整认知,于是很多系统的架构设计总是受一种超现实的引力场影响,逐步脱离现实。

抽象刻画还有另一层的引申含义:进行最基础设计(比如建模)时,我们就必须对软件设计作出取舍(而非等到我们的架构需要变动的时候),软件设计并非对于问题的直观理解的全盘照搬。软件对于业务而言是效率工具,它的构建和维护过程也必须拥有足够好的效率,所以工程师天然有倾向于寻找简化设计的动机。任何事越轻、越快、越敏捷,资源的投放和回报的速率也越来快。但不同的工程师对问题的抽象思路是不一样的,很多工程师厌弃课本上的教学思路,也没有认真学习过正统的 OO 思想,最后各师各法。这种实践产生的大部分的面向对象程序设计只做完了 OO 思想的最后一环,即 OOP,工程师们只学会了如何“把行为包装进结构体”里,并没有学会 OOP 之前的 OOD,也没有学会 OOD 之前的 OOA。这也是有些人不会画也不喜欢画 UML 图、架构图的深层原因,他们对设计的理解像没有章法的素描,只懂得使用一些框与线。

各师各法容易犯的错误是,在“舍”的问题上比较随意,而在“取”的问题举棋不定。

面对一个复杂的领域,有的人只能抽象出五六个模型,在一个流程里对模型进行管理,其他的东西只能通过扩展属性和猴子补丁把系统绑成一个大泥球;有的人能够抽象出25个模型,其中有5个是聚合根,进而推导出若干个领域和限界上下文。

如果对比来看,后一个设计“提取到了更多的东西”,而前一个设计“丢掉了更多的东西”。前一个设计之所以“显得不完备”,是因为建模者没有完备的推理方法,在庖丁解牛的过程中抛弃了太多血肉经络,只剩下一些“大而硬”的骨头。舍弃过多的信息量而只是简单地搭建起一些“大骨头”的系统缺乏法度,业务一旦进入领域的深度区间,工程师应对复杂性的问题就苦于没有趁手的武器(因为原本应该出现在此处的解空间正交元素,没有被前面的设计制造出来),通常会用非常复杂的局部方案做急就章的设计。如果项目进度紧张,加入更多的程序员赶工,就更容易出现《人月神话》里提到的“加入更多的工程师只会让延期的项目更加延期”的悖论。有的架构师们开始意识到,需要一整套完整理论来专门解决工程设计的取舍问题,于是《分析模式》、《四色建模法》、《实现模式》、《企业应用架构模式》等书籍或理论相继问世,专门探讨“如何抽象”、“如何分类”、“如何取舍”,甚或“基于此的架构风格”。领域驱动设计还专门把这些设计活动冠以战略设计和战术设计这样正儿八经的名字。第一次见到四色建模法或者领域驱动设计的工程师往往会大吃一惊,原来在做设计的时候别人会考虑那么多东西,而且这些方法论各成一派,让人眼花缭乱。

大部分人学习这些“取法”时吃足苦头。《领域驱动设计》成书于2003年,它在工程师群体里的接受历史长达20年。简化它的理解成本的红色《实现领域驱动设计》迟至2013年才出版,作者还嫌没有把问题讲明白,在2016年又写了一本蓝色的《领域驱动设计精粹》。尽管全世界有几十万人前赴后继地学习这项理论,领域驱动设计仍长期被当作一个空有定义、缺乏参考实现的口号运动。一直到2022年的今天,还有大量的团队声称自己在“理解和实践领域驱动设计”,各辟蹊径地设计各种系统架构,最后交付的结果大相径庭,恐怕 Eric Evans 本人都分不清哪个方案更正宗。过高的学习门槛让这些工作能力难以成为人人掌握的基本功,也并不容易达成共识,于是架构师顺理成章成了刻界于碑的法度制定者。一个团队的工程活动变成了我们最常看见的样子:架构师负责搭搭架子,因为他能看到“别人无法看见的东西”,其他人负责接架构师拆解出来的工作任务。各出机杼的设计只能存在某些实现层面,在问题的整体看法上大家只能按部就班地相信架构师的专业判断。如果领域驱动设计有段位的话,有些架构师对它的理解只停留在二段水平,有些架构师对它的理解已经达到四段水平,这种视角的高低差异是学院派架构和野生架构出现的深层原因之一。

架构设计的“取法”也并不是越高大全越好,我们并不能断定“后一个设计”就是一个好方案。现代的架构或建模理论浓墨重彩地讲它们的观点时,用到的例子往往是年深日久的传统领域里的项目,这些项目的业务流程经过长期固化,已经较易于“套上标准方法论”。而很多新兴领域的业务流程和模型尚未定型,架构师并无多少成法可参考。有些架构师看待一个新兴领域或者新兴业务时就好像看到一段骨架,其很多细节并未显化,无法想象这个骨架演化下去最终会成为一头鲸鱼还是一只狮子。架构师学习一个领域也需要认知迭代的过程(如前所述),而设计不等人,有时候他们需要在认知迭代的早期就做技术决策。此时任何人做很详细的设计都可能误入歧途,业务的迅速的发展也可能把今日的模型抛弃。能看见“别人看不见的东西”架构师并不拥有无限视野和对未来的占卜能力,要在过度设计的诱惑和设计不足的犹豫之间徘徊,好像棋手面对一个棋盘难以落子。所以谨慎的架构师都能宁可留有余地而不做难以转圜的决策,保留决策上的双向门可进可退而绝不走进单向门。这时候有些聪明人采用这样的方法:把先验的事情和后验的事情区分开来,凡是先验的东西应该积极落地,后验的应该努力探索,不断让团队在奔跑中用高投入产出比的事情把架构上长长的留白填下去。

当然,哪些是先验的东西,哪些是后验的东西,绝不是随便说说就能决定的。解决这个问题比较好的方法是寻找别人的经验进行学习,是为对标(benchmark),为人后而不为人先。比较有意思的是,这种学习既要认真学习成功的经验,也要学习失败的经验。成功的经验里固然有能把路走出来的东西,失败的经验里也隐藏了非常多“想不到我也有翻车的一天”的错误。比如,马老师看到 Supercell 的架构经验,会得出“我要建中台”的结论;而很多工程师见到阿里建中台的架构经验,会得出“我不要建中台”的结论。当然,也有些师心自用的工程师会得出“不管三七二十一,老子就是要建中台”的结论。

软件架构要适应不同协同者的差异

架构是要解决分工的问题的,我们首先要解决不同的工程师的分工问题。

有些工程师是架构师,有些工程师不是架构师,有些工程师是前端工程师,有些工程师是测试工程师,有些工程师是高级工程师,有些工程师是初级工程师,如何设计一个架构,让这些人成为一个团队,舒适地协同工作呢?

这就要求系统本身有多维度的结构。一个系统应该由若干条横线和竖线切分成不同的局部结构。

首先,割线要把领域割开。现代的商业软件仍然是叠床架屋构建起来的,大家谈起一套软件使用的技术的时候,往往会谈到技术栈这样一种层次结构。技术栈的边界就代表了专业领域的边界,以前需要一个工程师来完成的复杂软件,现在可能需要 IaaS 层、PaaS、Saas层、存储层、后端业务服务、接入层、客户端协同构建。有的复杂系统甚至要把前台和中台割开,把业务的前后工序也视为不同的领域。复杂的系统,除了要做好专业技能的上的水平分层,还要按照模型的维护职责,划定系统间的边界(这就是现在领域驱动设计里拳不离手的领域划分)。这样分割终于可以让各类工作者的工作专门化,也让很多问题的解在不同程度上逼近了必然复杂度,俨然就是很多架构师梦寐以求的“本质解法”。一个不恰当的架构里,很可能出现不恰当的切割线,使得不同工作者权责利纠缠在一起,让一方对另一方的地盘指手画脚,产生令人讨厌的藕断丝连。不恰当的边界难以避免,领域驱动设计都无法彻底解决。通常,一个完整的架构域经过长期演化,能够保证百分之七十的边界划分清楚,就已经非常不容易。在解这个问题的时候,有些架构师能够妥善应用 MECE 原则,按照金字塔原理对架构进行拆分,设计出来的系统像稳定的大厦或者一棵倒置的大树,就算相当不错。

其次,割线要把不同熟练程度的工程师能够工作的领域区分开。很多公司都对工程师的专业熟练度有不同的划分方法,一个工程师进入一间公司,负责的 scope 可能是一个模块,可能是一个系统,也可能跨越多个甚至多套系统。不同熟练程度的工程师能够在单位工作时间里驾驭的复杂度是有差别的,于是“怎样给他们分配合适的工作”,也成为了架构设计必须考虑的问题。有鉴于此,以前的复杂系统架构非常注重模块级设计(这个考量在某些日企的外包软件设计里体现得尤为明显),当代的架构师在设计复杂系统架构时,干脆变得非常热衷于拆系统。如果把架构的演进和组织的生长进行类比,一个技术团队的微服务变得越来越多,工程师也会越来越多。越来越多的微服务变得需要而且可以被普通工程师维护,越来越多的普通工程师也越来需要足够简单和独立的 scope 以专注工作。有时候有些团队的员工能力参差不齐,切线的设计还不能比照其他经典架构(按照其他经典架构的设计原则,水平分层的切线必须比照关注点的分离(separation of concern),最好按模块的类型切分),需要针对实际情况量身定制,那更考验架构设计因地制宜的能力。

当然,这些切割线的出现也没有统一标准,需要考虑现实团队的实际情况加以论证。正如上面我们谈到的,分工问题过于复杂,所以现在过少和过多的系统拆分都可能走火入魔。过少的大服务对于大团队而言是个拥挤的集市,不能支撑敏捷开发,过多的小服务在降低局部复杂度的管理难度的同时,又让全域复杂度呈指数级上升,将很多模块间交互问题转化为系统间交互问题,需要引入许多分布式场景的稳定性保障措施(按照《演进式架构》的观点,被拆分的服务可以被称为架构量子,架构量子的数量超过可以被手工管理的数量的时候,微小的服务颗粒度就突然变得成本高昂而好处寥寥)。这些年在各种技术大会里,形形色色的分享者们热衷于谈论自己为团队设计了一个多么大的系统,或者多少个五花八门的微服务,却不能讲清楚这样的架构演进到底给分工和生产力提升带来怎样的帮助,可见架构设计之难。探讨这个问题探讨得比较深刻的是《The Art of Scalability》(中文名《架构即未来》)这本书, AKF 立方体框架把大部分的理论和实践都包罗进去,逐渐有了越来越多的拥护者。

比较容易被忽视的是,我们还要解决工程师和非工程师的沟通问题。

现如今的互联网产品经理都很熟知福特宣传汽车的例子:当客户认为自己需要一匹更快的马的时候,福特告诉他其实他需要的是更快的速度,而不是一匹在世界上不存在的马,然后福特把汽车给了自己的客户,要他学会对产品的思维转变。这个例子很好地说明了解决方案制造者要面对的困难:自己实际产出的解决方案可能和最初客户要求的解决方案不是同一个东西,但本质上又必须是同一个东西。软件是为软件工程师所制造,而又要服务于所有 stakeholder 的多维复合体,每个人都觉得自己有必要在一个项目里讲两句。比较理想的状况是:所有人在软件设计沟通的时候使用同样的语言,能够保证大家始终探讨的是同一个问题,这也要求所有人对产品的组件、功能有清晰的认知,没有歧义。有的人能够接触到的软件是实际运行的产品,有的人能够接触到的软件是一些产品描述文档,也有些人能够接触到的软件是真正运行的代码。不同角色有其自身视角出发的观察方式,也有其独特的描述语言。不做架构师不头痛的一个问题是:软件工程师和非工程师对“所见与所得”这两个问题常常得出相左的意见,甚至引发旷日持久的争吵。我们都知道,只有使用了编译器才能让程序员写出合乎语法、逻辑正确的程序,只有这样的程序才能在处理器上正确运行。但世界上并不存在从客户需求生成产品经理文档的编译器,也不存在从产品经理文档生成软件设计的编译器。每个人都在关注产品本身,但每个人眼中同一个软件的样子却不一样,所以有些项目以如下的死亡螺旋无可挽回地走向失败:一开始产品的一些概念没有界定清楚,而软件研发过程中的建模、设计、项目跟踪又产生了各种各样的信息偏差,研发周期里出现一些蒙眼狂奔的流程,然后产出一个很难用但又很勉强能用的东西。这个东西看起来好像最初客户和产品经理想要的那样,但又有一些与预期不一致的地方。大家好像知道为什么这个东西难用,但又很难讲清楚难用到底是为什么。

软件设计是很难向非程序员讲清楚的。一个系统里运行的模块通常是用数学思维加上拗口的语言写的,程序员非经过训练不能看懂。一个大型程序如果没有好的架构,只要经过很多代程序员的维护,就会变得好像地质岩层一样斑驳复杂,经过训练也很难读懂,把它讲明白需要拿出地质考古的毅力来。试图给其他人讲明白这个程序是怎么运作的,哪里可以改动,哪里不可以改动,哪里可以这么改动,哪里不可以这么改动,都需要很多“老鸟”的口传心授。但这种碎片沟通一般不能把系统的全貌描述清楚,也不能讲明白系统的发展脉络,通过这种语言描述一个系统就好像管中窥豹,难免有局限性。而产品经理要了解这个系统的实际情况,经过这种“以其昏昏使人昭昭”的转述,自然也会得到有偏差的信息。另一方面,程序的遗留设计(legacy design)和产品经理一开始的预期就可能不一致,但为了给产品经理交差,很多程序员也会表面上答应了产品经理的直接需求,实际上做出了另一个方案,通过某种程度上的“转译”来间接达成产品经理的预期。能够巧妙地弥合预期产出和实际产出的 gap 未必是坏事,但“很难向其他stakeholder讲清楚这个软件”很容易导致熵增难以收拾,最后所有的需求都很难做下去,产品和研发的矛盾也就开始了。

想要让“所见与所得”相匹配,并不是要求程序员对其他 stakeholder 的要求照单全收,但一定要有办法让程序的逻辑以一种外行人也能看懂的方式呈现出来。这就要求这个软件设计不仅是合理的,而且还是可以被简单解读的。我们人类其实早就发现了解决这类问题的解法:复杂的结构应该是支持透视的,透视能够让我们看明白复杂结构的好坏。很多人很早就搞明白了语言是线性的而不适合描述非线性结构这一弊端,提倡“用图表说话”。软件工程领域的大师们也很早就发现了 diagram 的重要,而且他们发现软件不能使用单一视角描述,软件是横看成岭侧成峰的,于是 UML 在一开始就有 9 种视图。当然 UML 一开始也是为了程序员设计而不是专门为其他 stakeholder 而设计的,但这类图表是比较易于为其他人理解的已经为工程实践所证实。要让一个设计能够在多种视角和语言之间反复切换而不产生歧义,使用统一的图表语言已经成为很多产研团队实践中的共识。如果一个软件团队使用“4+1”视图构建架构基线,其中的用例和场景脚本描述往往必不可少;而有些团队使用 TOGAF 之类的架构描述语言描述自己架构基线的时候,在技术架构之外还会加上业务架构的图表,来让大家对系统的功能有一些非线性的认知。这些图表必须驾驭整个设计,对所有图表的阅览者而言,这些图表就是“所见即所得”,它的存在让产研工作易于沟通。只要上了图表,架构上的错误和丑陋就无所遁形,这又会倒逼架构设计者不断维护这个架构的功能合理性,努力在设想和实现之间保持微妙平衡。而好的设计也会让其他 stakeholder 清楚明白这个系统的每个用例的流程是怎样的,每个用例涉及的模型有哪些,它在生命周期是怎样变化的,不会出现“福特的客户竟是我自己”的情况。学会读懂一些基本的架构图和画一些流程图,甚至已经成为了一些产品经理的必备技巧。 到这里可能有些读者会意识到一个问题:看起来软件大部分时间是写给计算机程序运行的,但软件设计大部分时间是写给人看的。做好的设计不仅要有对计算机编程的思维,还要有对人编程的思维。一个很烂的程序的背后,不仅有很烂的图表表达,也有一团乱麻的思绪。

更容易被忽视的是,我们还要解决现在的程序员和未来的程序员的沟通问题。

很多程序员都承认一个事实:一个程序在最初的时候是最好写的,随着维护的深入,它会变得越来越难写。一个程序员看到一段非常复杂的逻辑的时候,有时候难以理解为什么它要这样拐弯抹角地解决一个特定问题,有时候甚至连为什么存在这样特定的问题都想不明白。有的程序员受限于自身水平,不知道怎么设计一个好的解决方案,于是写下了一段蹩脚的代码。也有的程序员接手一个救火项目,江心补漏无计可施,不得不写下了兼容旧方案而又支持新场景的补丁。还有的程序员因为一些外界的软硬件的“硬因素”限制,不得不解决一些“看不见的问题”,很多年后这些“硬因素”已经消失,解决方案仍然还在。这些解法往往难以更改,让人不喜欢却无可奈何,而且它们绝不会随着业务向好而变得越来越好。因为大部分人写下它们的那一刻就已经决定欠下技术债务不还,它们只会安静地躺在代码仓库里,等待有人发现它们奇奇怪怪的那一天到来。而且那一天到来的时候,人们会突然发现,原来坏的代码会产生坏的代码。也有一些代码使用了很高超的编程技巧,优化了某一个特定的问题,却因为种种原因和某个特定场景绑定在一起,等到维护者再次打开这段代码的时候,场景已经发生变化,而这段复杂的解决方案却无法被改动了,于是好代码也会产生坏代码。

程序员们很早就搞明白程序的 maintainability - 可维护性是一个问题。有些人把可维护性问题当作可读性问题来看,可读性问题的大致定义是:怎样用最简单的语言给未来的人讲故事,让你意图能够正确地传达到后来者的思维里,他不再有你的信息量,却仍然要在你的工作基础上做决定。换言之,你能点亮无法与你直接交流的程序员头脑里的灯吗?

第一个常见的解法是教会所有程序员写好注释。StackOverflow 社区里有个著名的问题What is the best comment in source code you have ever encountered? [closed],历数了程序员在写不好程序的时候通过写注释来挽回颜面的种种手段。有些软件工程理论认为好的代码应该是自描述的(self-explanatory),所以代码本身被称作内文档(inner document),如果代码本身的表达能力不够,才需要引入注释。大部分人读程序的时候也确实如此,我们只关注程序可运行的部分,对程序不可运行的部分兴趣不大,所以大部分人不喜欢写注释,也写不好注释。很多时候大家遇到讨厌的解决方案,与其写下详细的注释来补完这个设计,不如写个 TODO 安慰自己早晚有一天会回来修复这个问题。但大部分的 TODO 会变成永远的 TODO,很多遗留代码里的注释甚至是被简单拷贝复制粘贴过来的,只会误导后续的维护者。指望程序员写不好代码的时候能写好注释通常会失败,而且让读者痛苦的并不是注释,是注释也不能修复的设计问题本身。

第二个常见的解法是教会程序员重构。原始的重构教条涉及的都是小模块的变更,关乎怎样重新设计若干个对象之间的外观和行为,隐含着对合理的原则和合理的框架的要求。也就是说,对于由表及里牵连甚广的设计问题,重构并不适用。所以大部分人嘴上说要重构,实际上是在重写。对于复杂架构的重写也不是一件轻松的事情,以至于最近很多架构书籍专门发明了一个新词“架构重构”(Architectural Refactoring)。合理的重构首先要考虑外部兼容性,被逼到穷途末路的程序员会采取激进的策略,经常把一切推倒重来,引发次生问题。一个可重构的程序一开始就应该是易于重构的,重构的问题又变成一个怎样设计一个可以被重构的架构问题。

有人因此认为,程序的可维护性看起来是一个整洁代码(clean code)问题,它背后还藏着一个整洁架构(clean architecture)问题。一个软件要处理的领域的必然复杂度是一回事,它表征出来的复杂度是另一回事。在程序开发的古典时代,Unix 程序员是通过把难解的问题的高手解法封装进框架代码本身,只把可扩展部分暴露出来,来简化降低编程的门槛。使用 K&R 或者谭浩强的教材入门的程序员,在课堂上学到的一个程序是如何写一个“hello world”。一个“hello world”看起来只写了一行代码,但底层依赖的操作系统和标准库代码可能高达两万行。写一个 “hello world” 的可读性很高,也很难出错,所以很多人认为 Unix 是优秀架构的典范,Ken Thompson 和 Dennis Ritchie 是世界上最伟大的架构师,他们构建的 Unix 系统可以让架构师和其他维护者跨越时空对话,彼此永远也不认识。类似的例子我们也可以在当代的软件行业里见到很多,Spring、RESTful API 规范和各种 ORM 框架的出现让受困于 EJB 的程序员一下子变得很容易开发出易于维护的企业级程序,因为大部分的规范已经被框架制定好了。如何设计 bean,如何使用 ioc,如何设计切面,一个 API 应该叫什么名字,一个 DAO 应该怎么设计,都有约定俗成的范式可寻。一代程序员处理基本问题的思路可以被下一代程序员快速地理解,大家都觉得 convention over configuration 的时代来临了。写出很烂的代码的影响很小,因为每段代码都被框架压在一个小范围里。

可能很多人都没有意识到,正是因为这个时代有那么多现成的 CRUD 架构和规范可以采用,才产生了那么多的 CRUD 高薪岗位。但还是有很多业务的问题过于复杂, CRUD BOY 们无法简单处理,如果架构师本人还是编程高手,还会继续使用把“能力内建进架构的通用部分”的思路来追求架构的整洁,力图把“平台的代码和业务的代码分离开来”。食髓知味的程序员会在通用框架的基础上追求业务框架,追求“通用能力的沉淀”和“软件要素的复用”,于是产生了 COLA、SOFA等业务脚手架框架,也产生了各种业务中台、技术中台。这一代程序员遇到一个问题不要紧,我接下来会想办法把它的解决方案封装起来,下一代程序员将只知道这个问题被解决了,而不必关心这个问题是怎么被解决的。架构通过自净化来抵御熵增,把复杂度装在水平线下,受益于此,大部分程序员终其一生都不需要读 Spring 和 JRE 的源码。

架构是用例的回答,所以架构要包容所有相关用例

我们很难有穷举完我们的业务的用例的时候,我们也不可能一次实现完所有用例,所以我们的架构必须有包容能力,能够面向未来编程。

人类最初学习数学的人生体验大同小异:我们可以用穷举的方式来理解从一到十的加减法,因为我们的手指恰好就是一种可以适应这种穷举模式的计算系统。但如果要设计一套计算器来容纳所有的自然数的四则运算,我们就不可以继续使用我们的手指系统,必须另起炉灶了。我们的祖先给出的第一个答案就是算盘。算盘是一种可以用演绎的方式来进行正整数加减法的工具,它用有限的必然复杂度,支持无限种组合模式,而且任意组合的偶然复杂度可控。但后来人们发现算盘的计算能力也有限,因为我们还需要处理非正整数的计算问题,我们相继发明了了计算尺、手摇计算器、计算机和现代的 matlab(matlab是一个很神奇的东西,大部分的知乎用户都认为中国人自己无法设计和实现这么复杂的软件)。

计算系统的演化恰似我们系统架构的演化。任意一套系统的合格标准之一,就是它能够解决特定领域内的所有问题,哪怕今天这个问题没有出现,等它明日出现的时候,也可以被我们的系统解决。如果某个问题不能被我们的系统当场解决,它也应当被稍加修改的系统解决。

这种架构特性被很多架构理论称为 extendability - 可扩展性。它们把寻求 extendability 定义为“怎样使用最小的维护成本来新增所需功能”的问题。一个可扩展性好的架构,好像一个有无数仓位的架子,当出现新的业务需求的时候,架构师只要把业务拆解好,让自己或其他工程师把业务放进指定仓位即可,如果一类业务不能放在原本尺寸的仓位里,这个架子也有制造无限多的新仓位的可扩展空间,新旧仓位无耦合,独立运作与发展。业务之间拥有同质性,这就要求架构能够适应这种同质性。譬如一套电商系统今天能够支持苹果的交易,明天也应该能够支撑西瓜的交易,如果之前苹果无需退货,而西瓜需要退货,架构也应当满足这种业务诉求。某些中台架构师把业务装入架构的行为称作“一个业务使用了一项商业能力/平台产品”,把新增一类功能叫作“新增一项商业能力/平台产品”。如果一个架构已经设计并实现了所有的的商业能力或者平台产品,可以被看作“能力封顶”。

可扩展性要求我们的系统有扩展点,简单的可扩展性需求要求我们有简单的扩展点,复杂的可扩展性需求要求我们有层次化的扩展点,甚至还需要与之配套的配置系统。现代的软件工具设计者们,很早就意识到程序的可扩展性的问题。面向对象程序设计里控制复杂度的方式是,通过允许继承,让程序组件表现出多态的行为。这种设计模式让一个架构多出了无数扩展点,所以面向对象程序设计在诞生之日起就大行其道。一个复杂的系统如果如上述的架构思路设计为一个树形结构,则从树的叶子节点向上,每一层都是可扩展的。架构师可以在对解空间认知迭代后,通过扩展某一个层次来实现带有新必然复杂度的原子能力,也可以通过配置化的系统,实现对原子能力的组合,依靠有限的偶然复杂度交付完整的系统流程,这也就解决了“怎样使用最小的维护成本来新增所需功能”这个问题。

很多架构师在寻求可扩展性的道路上探索得非常远,从最初的 eclipse 的插件化架构出发,找到了不同的落地路径。阿里出现了 TMF 和星环,美团也出现了 BPF。现在各种中台/平台化架构,已经可以在分布式场景下,让一个系统将另一个系统视为扩展点甚至插件,通过全域的编排能力来交付复杂流程。这既满足了之前提到过的合理的边界拆分的前提,也保证了系统总能包含相关用例。这种架构的思路,往往是全域复杂度堆积到一定程度的推导重来的结果。要找到合适的扩展点,需要对业务核心能力非常强的洞察,懂得在大尺度上做正交分解和组合,无法实现能力和接口的标准化,也就无法实现可编排的架构,稍有不慎就会设计出大而无当的系统。

理想是美好的,现实是骨感的。也有很多的系统架构,总是受到业务剧烈变动的冲击,这产生了这个时代特有的“外部适应性”与长期主义的辩证关系。

如果把互联网公司的历史写成一本书,读者翻到第10页的时候,很难想象第100页写的是什么。一家公司10年前可能在做团购,10年后可能就在做无人机了。这些年流行的“互联网思维”,一夜之间突然想把所有的事情都拿互联网公司的做法重新做一遍。业务没有边界,意味着有些领域要处于长期拓荒状态,有些领域又要经常和其他领域联通。在这种高度不确定性的背景下,很多业务的规划和策略变来变去,让系统设计置身于高度不确定性的选择之中。大家习以为常把“拥抱变化”挂在嘴边,每个团队每天前进三十公里,但事后看地图,发现行进的路线好像一条无法直线入海的河流,充满了几字形的反复。一个团队抱着试错的心态去接触某一个新业务,突然发现自己试对了,赶紧在某个领域的建设里加大投入,但过几年发现又试错了,于是又要调转船头,转而建设其他东西,留下前面的能力半途而废。打个小镇做题家式的比方:一个高中课程所有的知识点总共有1000分,高考使用100分钟的考试来随机出题考其中的100分的知识点,这时候一个人能够拿多少分取决于他把1000分的知识点学了多少,这100分钟的考试时间里,考卷上的100分和自己学会的知识点的交集又是多少。小镇做题家解决这类问题的思路是,努力苦学,争取把1000分的知识点学会。但做业务系统没有这种预设的封闭与公平:一个团队的组织力大概只能做出300分的题目,业务的考卷可能有10000分的题量(这10000分就是我们需要做的选择:在一个近乎没有边界限制的市场空间里,我们要做哪些业务,这些业务需要多少种能力帮助落地,我们如何建设这些能力),答题时间也长达1年,这个团队如何拿到100分?如果还有其他竞争对手,各自的组织能力不仅分数不一样,而且偏重也不一样,本团队将如何乱中取胜?一种解决问题的思路,是像小镇做题家做更多的题目一样,继续努力苦练基本功,让自己的组织力从300分往10000分逼近;另一种思路是把考卷剪枝,让10000分的考卷往300分的能力逼近。采取前一种策略吃力不讨好,很多团队会采取后一种策略(在产品层面还有专门的方法论,比如 pmf 和 stp),但这种选择成功率不是很高,大部分的时候容易选错;也有很多时候市场的年景发生特别的变化,答卷里分值最高的知识点恰好就是本团队不具备的,本团队非把自己300分的组织能力提升,以适应外部变化不可。出现很多的摩擦和拉扯不可避免,原本需要长期可用的能力的价值因此无法自证,“架构方法论”就变成了一种佯谬,因为资源的投入拿不到结果。

不变的东西是事实上存在的,只不过不存在本本主义里而已,亚马逊就提倡把战略设定在恒定不变的东西上。也就是说,10000分的考卷里,有一些知识点是必考的知识点。按照某些方法论的观点,这些东西是某个市场的核心能力,或者关键输入指标的控制能力,这些能力就是握住未来的所有用例的线索。但找出这些不变的能力是很难的,需要反复的认知迭代,而且会经历很多进道若退的曲折。做架构师要有对领域发展的长期视野和专业判断,专业判断不简单迷信方法论,依赖于实事求是的分析和 trial and error(到底这个能力是否真的可以对业务如此重要,需要确实的业务结果支撑,而且也需要其他stakeholder最终买单),而且在遇到几字形的反复的时候,要有非常强的定力,在反复中也仍然坚持建设可扩展的架构,努力适应短周期的变化,把变化的东西在长周期装进不变的架构里,达成适应外部和长期获胜的平衡。

软件架构还有很多意想不到的问题要考虑

很多架构师认为,业务复杂度之外还应该考虑技术复杂度。还有非常多的东西,是技术复杂度这一范畴不能简单概括的,我们姑且把它们先称为“其他维度属性”。“其他维度属性”又多又乱,有些维度隐藏得很深,让人意想不到。

很多 system design 面试题,都会让设计者“设计一个淘宝”或者设计一个“twitter”。这些系统要实现的功能非常复杂,这要求架构者天然拥有上文提到的应对业务复杂度的能力,除此之外这些面试题里还隐藏有其他陷阱:架构者如何识别这个场景下的性能瓶颈和隐式约束。架构设计中的性能瓶颈已经涉及非常多的细节,隐式约束更是难以捉摸。

譬如,一个推特系统每日产生若干张图片,理论上产生的图片数会超越某些存储方案的极限值,你会如何处理这种问题?如果引入某个存储方案,如何在解决它的可伸缩性问题的基础上,解决数据可靠性问题?如果你选择了某种共识算法,它将在什么时间界限内满足哪个级别的一致性?你的方案达成一致是同步还是异步的?你将如何权衡读一致性和写一致性的成本?

譬如,一个推特系统要设计一个开放 API,如何保证它的安全性?你将使用多强的加解密算法或散列算法?你如何权衡某些密码学问题的加密强度和资源密集型应用之间的关系?你的 API 如何恰当地在区分了资源的边界,支持各种各样的其他系统通过多种协议接入?

譬如,这个系统的可管理性如何?可观测性如何?如何衡量它的可演化性,你是否有意识地使用健康度函数和引导变更机制来持续演进它?

譬如,你所在的团队使用了某个细分行业专有的存储解决方案,但此时此刻有一种上云的方案可以减轻你的成本,你是否愿意舍弃某些特性,转而上云?或者反过来,如果有必要,你是否会离开公有云,寻找自己的 ldc 的解决方案?如果你所在国的政府禁止你使用某种解决方案,你将如何切换你的系统架构到替代方案上-在切换之前,你的系统是可移植的吗?

譬如,今日你设计的架构只能给出某些核心能力,但公司的长期战略是要在若干年后达成若干目标,你的架构设计是否能够孵化出达成这些目的的核心能力?你将如何兼顾长期视角和短期的落地诉求,既不放过眼前的业务机会,也能够让架构演进终达到目标?

譬如,今日突然有了一个需要引入缓存的场景,你作为架构师,你要如何取舍近端缓存和远端缓存?这再次说明了系统设计里任何细节都可能是架构设计的一部分。

通行的架构理论都认为架构应该满足“可用性”、“可靠性”、“可测性”、“高性能”、“一致性”等通用特性,除此之外,还有若干个诸如“管理性”、“可复用性”、“可测试性”之类非常难以理解的质量属性,让人读得晕头转向。现实之中做架构决策时要考虑和处理的“xxx性”更是多得让人抓耳挠腮。我们很难讲一个使用了 Oracle 的某类特性而固若金汤的银行系统不是一个好架构,我们也很难讲一个去掉了 Oracle 从而每年节省了若干成本的架构不是一个好架构。而要深刻理解某类架构的优劣,需要架构设计者有深厚的理论基础支撑,也要有非常多的架构实践经验。这些经验和支撑既需要长久的学习,也需要大量的实战经验。从这个视角来看,架构既是学问,又是实践

适可而止的设计、恰如其分的架构与成败论英雄

到此为止,我们发现架构设计不是在解决一个问题,而是在解决若干个打包在一起的相关问题的集合体。书上的架构理论并不能解决我们的全部问题(亚马逊的工程师几曾感慨,几年的 SOA 改造经验让他们学到了在书本上永远学不到的东西)。因为资源有限,我们很多时候不能推导出全部问题。即使推导出了全部问题,我们也未必能够推导出全部答案。即使推导出了全部的答案,我们也未必能够把答案全部落地(有些答案甚至是相互矛盾的)。我们需要权衡的东西有十万个为什么,问题永远答不完。

于是乎,现实的世界里,人人都是架构师。学院派架构师设计学院派架构,野生架构师设计各种野生架构。每个团队都有自己独特的问题要解决,做架构设计的时候要相信自己实事求是的判断,而不是盲从别人的架构理论和实践。架构设计既涉及长期思考,又涉及短期思考,很容易套用 SWOT 的决策框架分析:为什么这个事情就应该由本团队来做,我们有哪些机会和优势值得让我们能投入(SO战略),我们有哪些劣势和威胁逼迫我们不得不做这个事(WT战略)。我们到底是要延伸我们某些能力的臂长,还是修补我们木桶的短板?这是摆在每个架构设计者桌面上的问题。很多公司每年都会做大量的业务战略规划,进而推导技术的滚动规划,试图平衡 SO 和 WT 两类问题,但最后大家发现,很多时候能够解决 SO 问题就相当不错了,WT 类问题不到暴雷的边缘不会有人收拾。在朝生暮死的互联网领域,很多技术架构的存活周期非常短,习惯了传统的企业级 Java EE 架构的架构师看到某些 TO C 场景下的架构设计,甚至会有朝菌不知晦朔,蟪蛄不知春秋的感慨。一个电商团队的架构,可能还没有演进到能够支撑百万量级的用户的阶段,就因为业务被取消而被废弃。在这种场景下追求高大全的架构原则的实践,远不如帮助企业抓住业务机会重要,怎样让架构适应外部变化成为了唯一值得考虑的问题。这时候被团队新成员骂的架构,只要能够维持业务跑下去并最终走上正轨,摇身一变居然也成了一种好的架构了。

这些年新出版的架构理论,已经开始承认这样的现实:“我们不能处理所有问题(更谈不上拿到所有好处),只能应对我们需要迫切解决的风险,做一些适可而止的设计”,这种架构思路被称为“恰如其分的架构”。这种理论认为,最初人们构建的系统,其实是使用通用架构设计出来的,是为“架构无关的设计”。再往后人们发现了很多特殊的核心能力或者质量属性可以嵌入到设计里非业务的部分,才产生了“提升架构的设计”。架构范式会在这种流程里慢慢浮现出来,这时候某些早期架构决策变得非常重要了,才出现“专注于架构的设计”,进而出现“参考架构”和“推定架构”。参考架构是可能成为事实标准的架构,它尝试对特定领域的架构方案给出一些固定解法,比如一个电商域的架构应当拥有营销域;推定架构是已经成为事实标准的架构,比如一个电商域的架构肯定要有商品域和支付域。大量的架构演进实践告诉我们,复杂而完整的大型架构并非不可达成的,但它们的落地也不能一蹴而就,需要投入很多资源,也要甘冒很多风险,经历许多曲折。有些架构师在分享的时候取标题《如何在战火中建造一座城市》,并非虚言。

我们应当警惕技术流行里的宏大叙事。很多“高大上”的架构理念一面世,立刻就引来一窝蜂的模仿者。仿佛旧的架构设计方法已经落伍,拥抱新的理念才能取得项目的成功,使用旧设计的架构终将失败。这个行业前几年动辄“使用中台助力传统企业软件架构转型”,过几年又闹着“要平台化,追求可配置,学 TMF”,这几年又开始大喊“中台已死,DDD 才是未来趋势”,浑然不顾 DDD 是一个早于中台诞生的设计理念。这些理念彼此之间并无高低优劣之分,也绝不是什么魔法咒语。声称只要使用了某项技术,就能神奇地解决不可解决的问题,只是一种技术迷信。比所有哺乳动物强大得多的霸王龙灭亡在白垩纪,真正在岁月长河中幸存却是弱小而灵活的哺乳动物。MVC 是一个已经诞生近40年的设计模式,现在仍然是很多企业应用架构的首选架构模式,并没有完全被逐渐流行的 DDD 分层架构所取代。而曾经甚嚣尘上的很多种 enterprise pattern,如今却已销声匿迹。求全必毁,过犹不及,时间会证明哪些架构方法是合适的。

到此为止,本文的基本结论已经出来了。好的架构能够让各种业务用例用恰当的模块分工落地,让利益相关者在付出最小的使用和维护成本的前提下,得到最高的生产力,创造最多的核心价值。这需要架构的所有利益相关者(用户、维护者)的一致认可,最后我们只能在力所能及的时间周期里,以成败论英雄