文章来源:https://my.oschina.net/u/3874284/blog/4653188 著作权归原作者所有

前言

“架构制图”这词乍一听似乎有些晦涩,但如果提起“工程制图”,相信绝大部分工科背景的程序员们都不会陌生,甚至还能共同感慨下那些年一起伏在宿舍左手圆规,右手直尺,徒手作图到深夜的日子。

软件工程也是工程,因此传统工程制图的一些基本理论,在软件行业同样适用。但另一方面,软件与实体制造业之间还是有着本质区别,所以在制图方面的需求和方式也大相径庭,无法直接套用。作为软件行业的从业者,你可以完全不懂工程制图,但你不得不懂架构制图 —— 这是任何程序员职业生涯的的必修课。

本文在后半段将介绍如何用图去描述(describe)和传达(communicate)你的架构设计。值得强调的是,本文并不会侧重于单一的方法和工具,而是更希望关注那些优秀方法背后的通用方法论,即架构制图的本质共性最佳实践。希望本文能起到引子作用,激发大家对自己日常工作中关于架构和制图部分的关注、审视与思考;如果还真能帮助大家提升一点点制图效率和效果,那就更好不过了。

什么是软件架构?

1. 软件架构定义

IEEE 给出的定义:架构是环境中该系统的一组基础概念(concepts)和属性(properties),具体表现就是它的元素(elements)关系(relationships),以及设计与演进的基本原则(principles)

CMU 软件工程研究院的定义:架构是用于推演出该系统的一组结构(structures),具体是由软件元素(elements)、元素之间的关系(relationships),以及各自的**属性(properties)**共同组成。

Uncle Bob 在 Clean Architecture 一书中给出的定义:架构是创建者给予该系统的形态(shape)。这个形态的具体形式来源于对系统组件(components)的划分排列,以及这些组件之间互相通讯的方式。

2. 架构核心要素

综合上述各种权威定义,软件系统的架构通常需要包含如下四类核心要素:

  • 元素(elements):将系统拆分为一组元素 - 模块、组件、结构体、子系统;
  • 关系(relationships):不同元素之间的关系 - 交互、依赖 、继承、组合、聚合;
  • 属性(properties):每个元素具备的属性 - 名称、职责、接口、实现限制等;
  • 原理(principles):为什么这么设计 - 拆分依据、设计原则、决策原因等。

为什么架构很重要?

1. 架构是系统实现的蓝图

最近有部很火的网剧叫《摩天大楼》,讲述了一段匪夷所思的悬疑故事。为什么扯这个呢?因为我想借用这个剧的标题来问个问题:摩天大楼是由谁建起来的?也许你心里会默念:废话,不就是建筑工人们一砖一瓦堆起来的嘛。仔细再想想?背后是不是还有一堆操碎了心的建筑设计师(比如剧中帅气的林大森)和土木工程师们?他们虽然不搬砖也不扛水泥,但如果没有他们产出的那些繁琐严谨的设计图纸,摩天大楼是是不可能像农村自建房一样仅凭工人们各自的经验与想象力就能快速平稳地竖立起来的。

正是靠着这些图纸所描绘出来的工程蓝图(blueprints),才让成百上千工人们的分工合作和验收标准有了依据:大家只需要照着蓝图,按部就班地把自己所负责的那些砖瓦添上去就行了;只要蓝图正确,且施工过程也没有偏差,最终顺利完工只是个时间问题。

与建筑、汽车或者任何其他工程行业一样,软件在落地实现(编码)之前也需要先有蓝图;而其中最重要的一份蓝图,就是架构设计。没有架构,仅凭程序员自己脑子里的模糊设想,也许你可以像传统手艺人一样独自创造出一些美好有用的小东西(比如 Linux 0.01 版本),但不太可能以工程的方式协同一个团队共同建造起一个与摩天大楼规模类似的复杂软件系统(比如现代的 Linux 系统)。一方面,人类的思维能力终归有限,必须依靠架构这种高度抽象和简化的蓝图,才能让复杂系统的创造、理解、分析和治理变得可行;另一方面,量级达到一定程度的大型系统,也只能依靠多人分工合作才能完成,而架构也正是多人沟通协作的重要基础。

2. 架构是沟通协作的基础

 

软件项目的最终价值产出就是软件系统,而架构作为软件系统的灵魂和骨架,可以起到如下作用:

  • 理解对齐:所有软件系统的目的都是为了实现用户需求,但实现的途径有无限种可能性(相比传统工程行业,软件的灵活性更大、知识迭代更快)。架构设计就是去选择其中一条最合适的实现途径,因此其中会涉及非常多关键的选路决策(为什么要这么拆分?为什么选择 A 技术而不是 B?)。这些重要的技术决策需要通过架构描述这种形式被记录和同步,才能让项目组所有成员对整个系统的理解对齐,形成共识

  • 工作量化:项目管理最重要的步骤之一就是工时评估,它是确定项目排期和里程碑的直接依据。显然,只通过 PRD / 交互图是无法科学量化出项目工作量的,因为很难直观判断出一句简短需求或一个简单页面背后,究竟要写多少代码、实现起来难度有多大。有了清晰明确的架构之后,理论上绝大部分开发工作都能做到可见可预测可拆解,自然而然也就能够被更准确地量化。当然,精准的工作量评估在 IT 行业内也一直是个未解之谜,实际的工期会受太多未知因素影响,包括程序员的技能熟练度、心情好不好、有没有吃饱等。

  • 标准术语:编程作为一种具有创造力的工作,从某种角度看跟写科幻小说是类似的。好的科幻小说都喜欢造概念,比如三体中的智子,如果没看过小说肯定不知道这是个啥玩意儿。软件系统在造概念这一点上,相比科幻小说只有过之而无不及,毕竟小说里的世界通常还是以现实为背景,而软件中的世界就全凭造物者(程序员)的想象(建模)了。稍微复杂一点的软件系统,都会引入一些领域特定甚至全新创作的概念。为了避免在项目过程中出现鸡同鸭讲的沟通障碍和理解歧义,就必须对描述这些概念的术语进行统一。而架构的一个重要目的,就是定义和解释清楚系统中涉及的所有关键概念,并在整个架构设计和描述过程中使用标准和一致的术语,真正做到让大家的沟通都在一个频道上。

  • 言之有物:就跟讨论产品交互时需要对着原型图、讨论代码细节时需要直接看代码一样,架构是在讨论一些较高维技术问题时的必要实物(具体的实物化形式就是所谓架构描述)。否则,要么一堆人对着空气谈(纸上谈兵都说不上),要么每次沟通时都重新找块白板画一画(费时费力且容易遗落信息,显然不是长久之计)。

  • 知识沉淀 & 新人培训:架构应该被作为与代码同等重要的文档资产持续沉淀和维护,同时也是项目新人快速理解和上手系统的重要依据。不要让你的系统跟公司内某些祖传遗留系统一样 —— 只有代码遗留了下来,架构文档却没有;只能靠一些口口相传的残留设计记忆,苦苦维系着项目的生命延续。

3. 架构决定了产品质量

如何衡量一个软件产品的质量?上图是 ISO/IEC 25010 标准定义的软件产品质量模型,包括以下 8 个大类:

  • 功能适合性:功能完整度、功能正确性和功能恰当性;
  • 性能效率:时间表现(e.g. 响应时间)、资源利用和容量;
  • 兼容性:共存能力(e.g. 多版本组件共存)和互操作性;
  • 可用性:可学习性、可运维性、用户错误保护(e.g. 自动纠错)、UI 美观度、可访问性;
  • 可靠性:成熟度、可用性、容错性、可恢复性;
  • 安全性:机密性、完整性、不可伪造性、权威性和可审计;
  • 可维护性:模块度、可复用性、可分析性、可修改性、可测试性;
  • 可移植性:可适配性、可安装性、可替代性。

上述质量模型中列出的所有点,都是架构设计需要着重考虑的。其中除了功能适合性以外,其他所有点都属于非功能需求的范畴,这也是区分架构好坏的真正分水岭 —— 好的架构设计,不会停留在仅满足功能需求这一最基本的需求层次上(最坏的架构设计也同样能做到),更重要且更难以应对的是其他众多的非功能需求。

当然,鱼与熊掌不可兼得。架构与人生一样,也是一场权衡的游戏,弄不好就跟第八季的龙母一样的下场:既要又要还要,最后反而什么都得不到。好的架构师更应该像雪诺同志学习,表面上“know nothing”,实际上“know everthing”:清楚系统所有利益相关者(stakeholders),努力挖掘各方的主要述求(concerns),相应平衡自己的架构决策(decisions),最终实现你好我好大家好的终极架构目标。

4. 我还能说出更多理由

要不是篇幅所限,这一页 PPT 显然不够装:

  • 架构包含系统所有最重要的早期决策,这些决策会进而影响后续所有大大小小的技术决策。因此,早期的架构设计需要非常严谨和慎重,要尽可能“一次做对”(虽然很难),否则越往后纠错的成本越高;

  • 架构在组织内具有非常高的复用价值,因为同一组织内的产品之间一定会具备很多共性(需求、限制、环境等),很适合在架构层面进行最大化复用,避免重复解决相似的问题;

  • 康威定律指出,软件架构反映了组织结构。这个结论反过来也成立:好的架构也会让组织结构变得更高效

  • 越庞大和复杂的系统,架构越重要,因为只有好的架构才能有效控制、管理和降低系统复杂度

  • 是不是越听越糊涂,仿佛架构有无数种诠释和意义?不必过于纠结,按照GoF的设计模式所述:Architecture is about the important stuff. Whatever that is. 对,管它是啥,记住架构很重要就够了。

如何设计一个好的架构?

理解了架构的概念和重要性后,真正的架构师修炼之路才刚刚开始。如何设计一个好的架构?这显然是一个非常博大精深的主题,但并不是本文的重点,因此这里只简单列举了一些基本思想(原则)和经典套路(模式)。当然,架构设计更接近一门经验学科,仅停留在能脱口而出一些玄乎而高大上的理论概念肯定是不够的,需要结合实际工作内容和业务场景多多实践和揣摩才行,否则只能算是徘徊在架构的门外,连入门都谈不。

1. 架构原则(principles)

SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):

  • 单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;

  • 开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;

  • 里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;

  • 接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;

  • 依赖反转:依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦。

此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):

  • 正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠;

  • 高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开);

  • 低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性;

  • 隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分。

2. 架构模式(patterns)

架构模式(architectural patterns)与我们常讨论的设计模式(design patterns)并不是一码事,但如果仅从“模式”这个角度去解读,两者的理念都是一致的:针对给定上下文中经常出现的问题的通用、可复用的解决方案。最主要的区别在于,架构模式会更高维抽象和偏全局整体(毕竟是运用在架构设计层面)。

常见的架构模式,既包括一些传统模式(e.g. 分层、C/S、MVC、事件驱动),也包括一些新兴玩法(e.g. 云原生、微服务、Serverless)。不同模式有不同的适用场景,没有哪一种模式能通杀所有需求。成熟的架构师应该像一个冷静到冒得感情的杀手,永远只会客观地评估和选择最适合当下的解决手段,即使那么做会显得简单乏味;相反,不成熟的架构师,一心总想着搞事情(e.g. 强行套用微服务架构),而不是真正搞定问题。

怎么描述你的架构设计?

有了良好的架构设计,万里长征之路就已经走了一大半。就像是青年导演第一次遇上好剧本,心潮澎湃两眼放光,仿佛已经预见了电影上映后的票房盛况。当然,剩下的一小半路,并不会如想象中那么平坦 —— 同样的剧本,不同导演拍出来会有质一样的区别。好的“最佳导演”,即使面对不是“最佳剧本”的剧本,也有能力拍出“最佳影片”。同样,好的架构师,也应该有能力描述好一个不错的架构设计;即使做不到为精彩的内容加分,也不应该因为形式上没描述好而丢分,否则就会像高考作文丢了卷面分一样憋屈和心酸。

1. 架构描述的意义

为什么要描述架构?让它只存在我深深的脑海里不行吗?西方人有句谚语:好记性不如烂笔头。任何没有持久化的东西都是易失的(volatile),就跟内存一样。另一方面,就如前文所述,架构是沟通协作的基础,不通过架构描述(Architecture Description)沉淀下来让所有项目干系人都能看到,那就失去了沟通和传播的唯一载体。

根据个人观察,大家对“架构需要描述”这一点都没异议,所以绝大部分项目都或多或少会产出一些有模有样的架构描述文档。但“有架构描述”和“有好的架构描述”,这之间的鸿沟是巨大的,甚至比“没有”和“有”之间的差别还大。如果你也跟我一样,饱经沧桑阅尽无数架构文档,曾拍手叫好心怀感激过,也曾拍着大腿愤怒不已过,应该也能感同身受。

2. 架构描述的方式

对于同一件事物,作家会选择用文字来叙述,而画家却会用图画。尽管两者想要传达的信息是一致的,但描述方式的不同也会带来效果上的巨大差异。架构描述也分**文字(Text)图(Diagram)**两种形式,两者各有千秋:

  • 文字的背后是由一套严谨和完备的语言作为支撑,因此其描述可以做到非常精准详尽,而且编写起来也很方便,随便打开个记事本软件都能写;此外,就跟写代码一样,文字很易于做版本管理,借助简单的文本 diff 工具就能一目了然地对比出不同版本之间的细节差异;

  • 相比而言,图并不具备以上文字所独有的特点,但也有自己的独特优势:图是直观形象的,顺应了人类与生俱来的视觉识别本能;图的表达能力更强,很多时候一小张图所能传达出的信息(比如空间位置关系、颜色分类、图标形状),也许用一千行字也不足以完整准确地描述出来,即所谓“一图胜千言”。

聪明的你冷笑了一声:哼,又不是小孩子非得做选择题,难道不可以文字与图都要吗?当然可以,理想的架构描述一定是图文并茂的。但现实世界显然比理想残酷,实际软件项目中很难给你留足时间先憋出一篇完美的架构文档。如果以成年人的思维去考虑投入产出比(ROI),那么你一定会优先选择画图。

3. 为什么你应该优先画图?

敏捷软件开发宣言中提到:相比详尽的文档,可运作的软件更加重要(Working software over comprehensive documentation)。这么说当然不代表就不用写文档了,只是提倡没必要写过于详尽的文档。为什么?因为详尽的文档需要耗费大量的编写和维护成本,不符合敏捷开发的小步迭代和快速响应变化等原则。

那么,在如今这个全面敏捷开发的时代,如何也顺应潮流更加敏捷地编写架构文档呢?ROI is your friend —— 不求多,但求精,尽量用最少的笔墨表达出最核心的内容。从内容上来说,ROI 高的部分一般是偏顶层的整体架构或最核心的关键链路,这点在后文的 C4 模型理念中也有体现。而从形式上来说,图在文字面前具有无与伦比的表达力优势,显然是 ROI 更高的选择。

4. 为什么你需要学习画图?

多画图是没错,但有必要专门学习吗?又不是素描彩笔水墨画,只是画一堆条条框框而已,稍微有点工程常识的都能上。画的有点丑?那没关系,顶多再动用点与生俱来的艺术美感,把这几条线对对齐那几个框摆摆正,再整点五彩斑斓的背景色啥的,不就显得很专业了嘛?

看到这里,屏幕前的你又轻蔑一笑:哼,显然没这么简单。确实,道理说出来大家都懂,架构制图与工程制图一样,都是一件需要下功夫认真严谨对待的事情。但现实中大部分人还真没这工夫去下那功夫,比如上面贴的两幅很常见的架构图。第一张图不用多说,这种草图自己涂涂抹抹挺好,但拿出来见人就是你的不对了。那第二张图呢,看上去似乎还挺像那么回事的?并不是,如果你更仔细地去揣摩,就能发现这张图底下所隐藏的很多模糊和不严谨之处(可参考这张图的来源文章:The Art of Crafting Architectural Diagrams)。

所以,能画图并不代表能画好图;要想制得一手既漂亮又可读的好图,还是需要经过持续学习与刻意练习的,很难仅凭直觉和悟性就能掌握其中的关键要领。此外,错误的图往往比没有图还要糟糕,即使你只是抱着“有图就行,差不多那个意思得了”的心态,也至少应该理解一些科学制图的关键要素,避免给本来就已经很复杂难做的项目又蒙上一层模糊滤镜,甚至起到混淆和误导的反作用。

5. 架构制图的目标

讨论具体的制图方法和工具前,我们需要先竖立清晰的制图目标。工具是人类进化的阶梯,但如果理解和利用不当,则很容易反过来被工具所限制甚至奴役,忘了最初发明和使用工具的初心。对于架构制图而言,已经有那么多形形色色的方法与工具,使用它们的初心是什么呢?我认为本质上都是想把制图这个过程从一门自由的手艺变成一项科学的工程:系统、严谨、完整、标准化,同时能做到可重复、可持续和高效。

P.S:当时做 PPT 太赶,所以从这个章节开始的配图,只能被迫走极简路线了,还请见谅。。。

架构制图方法与工具

经过前面几个章节的“简短”铺垫,相信大家对架构制图的背景知识都已经产生了足够的认知。本章节将会具体列举和描述一些典型的架构制图方法与工具,其中有常见的也有罕见的,重点是希望能通过各种方法的横向对比,加深大家对制图方法本质的理解。

1. 方法一:UML

UML 应该是大部分人最熟悉的制图方法了,最新的 UML 2.x 版本由以下两大类图组成:

  • 结构图(Structural Diagrams):通过对象、属性、操作和关系等方式,强调系统的静态结构,其中最常见的类型包括类图(Class Diagram)、组件图(Component Diagram)和部署图(Deployment Diagram);

  • 行为图(Behavioral Diagrams):通过展示对象之间的协作关系以及对象内部的状态改变,强调系统的动态行为,其中最常见的类型包括用例图(Use Case Diagram)、活动图(Activity Diagram)、时序图(Sequence Diagram)和状态机图(State Machine Diagram)。

作为通用的“统一建模语言”,UML 总共包含了 14 种不同类型的图,可以全面覆盖软件设计领域各种制图需求,当然也包括了架构制图。同时,也正是因为 UML 把自己当成了一门语言,因此其各种记号(notion)和语义(sematics)都有非常严谨的定义,不会出现模糊或者歧义问题。最后,UML 经过几十年的发展和推广,也早已成为世界范围内广泛使用标准规范,其所带来的的隐性价值就是:在团队内使用 UML 进行沟通的成本是比较低的,因为可以假定绝大部分技术人员都能理解UML的含义和用法。

然而,UML 也非万能(虽然历史上曾一度把它当成软件设计的银弹),它最被人诟病的缺点就是过于复杂。这也不能怪 UML,毕竟它就是要被设计为足够通用、严谨和强大的,这些目标都与“简单”背道而驰,并让它一步步演化到了今天这个复杂刻板的庞然大物模样。虽然上面我们自信地假定了技术人员大多都懂 UML,但这个“懂”如果带上一个程度量词,我觉得平均能到 20% 就不错了 —— 绝大部分也就能认识几个常见的类图、时序图,估计都很难准确说出类图中各种箭头的含义。

无论怎么说,UML依然应该是每个程序员的制图工具箱中最常用和必备的工具之一。当然,也不应该是唯一,因为下面也还有些不能错过的好东西。

2. 方法二:4+1 View Model

“4+1”是啥?不知道没关系,听过“6+1”吗?对,就是那个小时候常看的“非常6+1”节目。它跟“4+1”之间的关系,就跟它们与邵佳一、张嘉译和沈佳宜之间的关系一样,除了赶巧共用了同一个后缀发音以外,八竿子打不着。

所以,“4+1”到底是指什么?让我们来 Wiki 一下:“4+1”是一种视图模型(view model),可以通过多种共存的视图描述软件密集型系统的架构。这些视图基于不同项目干系人(利益相关者)的视点(viewpoint),例如:终端用户、开发者、系统工程师和项目经理。“4+1”由 4 种基础视图和一些经过挑选的用例或场景(即额外的“+1”视图)组成,各自的具体含义如下:

  • 逻辑视图(Logical view):描述系统为终端用户提供的功能,一般会通过UML中的类图和状态图来表示;

  • 过程视图(Process view):描述系统的动态行为,包括流程和交互等,一般会通过 UML 中的时序图、活动图和通讯图来表示;

  • 开发视图(Development view):从程序员的视角来阐述系统,也被称为“实现视图”,一般会通过 UML 中的组件图和包图来表示;

  • 物理视图(Physical view):从系统工程师的角度来描述系统,包括系统组件的物理拓扑、各组件之间的物理连接,也被称为“部署视图”,一般会通过 UML 中的部署图来表示;

  • 场景(Scenarios):通过一小组用例或场景来描述架构,包括系统中各种对象和进程之间的交互时序,也被称为“用例视图”。这些场景会被用于识别架构元素(architectural elements)以及阐述和验证整个架构设计,也可以被作为架构原型的测试起点。

虽然上面提到“4+1”的各种视图一般都是用UML图来表示,但实际上“4+1”本身是一种通用的视图模型,并没有限制绘图的记号和工具。对于工程师而言,这种偏学院派的方法可能这辈子都不会直接用到,但其中蕴含的一个关键架构制图思想非常有价值:架构需要通过多种视图来描述,而这些视图是来源于不同项目干系人的视点(角度);只有这样才能产生一整套全面、立体且客观的架构描述。

3. 方法三:C4 Model

C4 模型是一种“抽象优先”(abstraction-first)的架构制图方法,它也是受前面的 UML 和“4+1”视图模型所启发,但相对而言要更加简单和轻量,只包含少量的一组抽象和图表,很易于学习和使用。

1)定义、理念与关键思想

C4 模型通过容器、组件、代码以及人这几个抽象来描述一个软件系统的静态结构,它的核心理念是希望像 Google Map 一样,通过不同层次的细节,为代码建立一种可以放大缩小的导览图。它最关键的思想就是自顶向下对系统的静态结构进行逐级拆分,依次描述各层次对象的职责、关系和外部依赖。除了核心的层次化静态结构视图,它还可以包含动态视图、部署视图等补充视图。

上面的左图展示了 C4 模型中各层次抽象之间的映射关系:1 个软件系统由 1~N 个容器组成,1 个容器由 1~N 个组件组成,1 个组件由 1~N 个代码结构组成。右图是以简单的 Spring PetClinic 项目为例,演示了一个真实软件系统在 C4 模型下的层次结构:最上层就是 PetClinic 软件系统,它可以拆分为数据库、Web 应用等几个容器;Web 应用又可以进一步拆分出 ClinicService 这个组件,而这个组件下又包含了 ClinicService 接口类、ClinicServiceImple 实现类、Owner / Pet / Visit 等领域对象类。

使用 C4 模型进行架构制图,本质上就是对上述几种抽象进行可视化。具体的做法是依次建立如下几类从粗到细的结构图:Context、Container、Component 和 Code(可选),这也是 C4 模型名称的来历。

2)Level 1:System Context diagram

系统上下文图作为第一级(L1),提供了一个展示系统全貌的**顶层大图(big picture)**视角,包括最中心的软件系统、周边的用户以及其他有交互的系统。其中最关键的两个概念分别是:

  • (Person):即使用软件系统的用户,例如一个在线商城系统的消费者、运营小二、系统管理员等;

  • 软件系统(Software System):作为最高层次抽象,描述了给用户创造价值的软件制品;既包括当前正在设计的软件系统,也包括该系统所依赖(或被依赖)的其他软件系统。一个软件系统通常是由单个软件开发团队所负责。

在绘制系统上下文图时,不需要关心诸如技术栈、协议等任何底层细节。这类图的受众是最广的,因为任何人都可以理解并从中获取到足够的信息,包括技术人员和非技术人员,也包括团队内成员和团队外成员。

3)Level 2:Container diagram

通过 L1 的上下文图理解了系统在整个 IT 环境中的定位后,下一步就是把系统这个框框放大,详细看下其中包含了哪些“容器”(Container,注意不要跟 Docker 容器搞混了噢!)。C4 模型中的容器是指单个应用或数据存储,通常可以独立部署和运行(有独立的进程空间,通过 IPC 机制互相通讯),例如:SpringBoot 微服务、React SPA、移动 App、数据库、Serverlss 函数、Shell 脚本。

L2 的容器图不仅展示了系统的进一步职责拆分,还包括了主要的技术选型、容器之间的通讯方式等关键架构信息。这类图可以面向全部的技术人员,既包括架构师、开发者,也包括运维人员、技术支持等。

4)Level 3:Component diagram

继续前面的套路,下一步就是把系统中各个容器再分别进行局部放大,将每个容器进一步拆分成多个组件(Component)。在 C4 模型中,组件是指一组通过良好接口定义封装在一起的相关功能(通常运行在同一个进程空间内),例如:Spring 里的一个Controller(不只包括定义了 REST 接口的 Controller 主类,也包括背后所有相关联的实现类,如 Service/Repository 等)。

与容器图类似,L3 的组件图也不只包含了容器的组件划分,还包括各个组件的职责定义、技术与实现细节等。随着层次的下沉和细节的增多,组件图的受众范围进一步缩窄,一般只适用于软件架构师和开发者(其他角色没必要理解,一般也理解不了)。

5)Level 4:Code(可选)

再继续对组件进行放大,所能看到的最底层和细节的信息,就是 L4 的代码(Code)了。当然,这里所谓的“代码”还是以图的形式(e.g. UML 类图、数据库 E/R 图)展示类或文件粒度的代码结构,并不是真正的代码本身。即便如此,代码图在 99% 的架构描述场景下也依然过于详尽,一方面数量庞大,绘制成本很高;另一方面易于变化,维护成本也非常高。因此,一般只有非常重要和复杂的组件才需要用到这一层级进行描述。如果确实需要绘制,也应该优先考虑自动化的方式,比如很多 IDE 就支持自动生成 UML 类图。

6)补充图:Landscape / Dynamic / Deployment Diagram

除了上述各个层次的静态结构图,C4 模型还提出了一系列的补充图(Supplementary diagrams),包括:

  • 系统全景图(System Landscape diagram):全景图与系统上下文图的绘制方法类似,区别在于它是从企业或组织角度全景地展示出所有软件系统(包括与当前系统没有直接关联的)以及相关的用户和系统交互,即进一步放大架构图的 scope

  • 动态图(Dynamic diagram):由于结构图天生只能描述出系统的静态结构属性,因此 C4 模型中推荐使用 UML 中的通讯图、时序图等,对系统中关键链路的动态行为进行补充描述,即“动静结合”;

  • 部署图(Deployment diagram):除了缺失动态属性,上述结构图还有一个局限性:只描述了系统的抽象逻辑架构,并没有描述出系统实际部署时的具体物理架构。因此,C4 模型推荐再使用 UML 的部署图,对系统逻辑节点(一般是 L2 的“容器”粒度)与物理节点(e.g. 物理机 / 虚拟机 / Docker 容器 / 应用 Runtime)之间的映射关系进行补充描述,即“虚实结合”。

结合了这些补充图后的 C4 模型,才是可以全面与立体地描述出软件架构方方面面的完全体架构制图方法。

4. 方法四:arc42

严格来说,arc42 并不是一种架构制图方法,而是一个架构文档模板。虽然如前文所说,在架构描述中“图”是比“文字”更高优的选择,但实际项目过程中你终究还是需要产出一份相对完整、有图有文字的架构文档。arc42 就是专门用于帮助大家更好地编写架构文档;而作为架构文档中最重要的架构图,显然 arc42 也不会放过 —— 其中多个核心章节都与架构图有关,且详细描述了相应的制图方法。这里不会详细展开介绍 arc42(不能抢了下一篇文章的饭碗),只会简单介绍下 arc42 中制图方法与 C4 模型的异同。

伟大的思想都是相似的,arc42 也不例外。上方左图的右侧部分,概括了 arc42 模板中与制图相关的几个核心章节,分别是:

  • 第 3 章 - Context:该章节用于介绍系统的背景和上下文,因此其制图思路几乎等同于 C4 模型中的 L1(系统上下文图);

  • 第 5 章 - Building block view:该章节用于介绍系统的基本构成要素,按照官方指导思想也与 C4 模型中的自顶向下层次化拆分思想无异,唯一区是 arc42 并没有规定拆分的具体层次,只要有需要可以按照“黑盒 -> 白盒”的套路一直拆到底;

  • 第 6 章 -** Runtime view**:看名字就无需解释了,就等同于 C4 模型中补充的运行时视图;

  • 第 7 章 - Deployment view:同样地,这里也等同于 C4 模型中补充的部署视图;但有一点,arc42 强调部署视图也可以类似结构视图一样做自顶向下的层次化拆分(对于较为复杂的部署架构,层次化确实很有必要)。

因此,本质上 arc42 中提倡的制图方法与C4模型是等价和兼容的,完全可以配合使用:以 arc42 作为架构文档框架,其中的架构制图采用更具体的 C4 模型。这也是目前我们项目中实际采用的方法。

5. 其他方法 & 制图工具

除了上述几种方法以外,在软件行业蓬勃发展的数十年间也涌现出过很多其他的优秀架构制图方法,其中既包括一些通用方法,如:SysMLAADLArchiMate,也包括一些领域特定方法,比如在企业中后台业务建模场景中很常见的 BPMN。再详细地展开描述各个方法,显然会让本文又臭又长(虽然写到这里时似乎就已经注定了),有兴趣的读者可以自行检索和探索。

到这里为止,本章节介绍的都是架构制图的各种方法;而实际从方法到落地的过程中,还有一个绕不开的环节:选用什么样的工具去制图?总不能真的跟写工程制图作业一样用纸和笔吧?作为数字化改革的推动者,程序员们当然要全面拥抱数字化工具;大家日常工作中必然也已经积累了很多顺手的画图工具,因此这里我只推荐两个自己用得比较多的:

  • draw.io:这是一个开源的在线绘图软件,相信很多人都有用过。考虑到数据安全问题,推荐大家用完全离线的桌面版。作为一个程序员友好的绘图工具,draw.io 的最大优点就是支持三方插件,比如这个开源的 c4-draw.io 插件,可以帮助你更方便地在 draw.io 中绘制 C4 模型架构图;

  • PlantUML:作为文本制图的代表性工具,PlantUML 可以用于绘制各种类型的UML图,以及其他一些适用于文本制图场景的图(比如这个开源的 C4-PlantUML 扩展)。在这些场景下,文本制图具有可视化制图所无法比拟的优势:轻量、高效、版本化、自动化、一致性、易于复用等。虽然文本制图工具诞生已久(比如应用广泛的 Graphviz,最早发行于 1991 年),但相信随着现代各种 XXX as Code 的意识觉醒,这类 Diagram as Code 工具也会获得更多青睐(btw,语雀文档早已支持内嵌 PlantUML 制图)。

架构制图方法论总结

古有云:授人以鱼,不如授人以渔。推而广之:授人以方法,也不如授人以方法论。什么是方法论?虽然这个词在公司里已经用烂了,但确实有它的价值和意义:方法论(methodology)是对方法的更高维度抽象,由它可以推导出解决问题的具体方法(method)。理解了方法论,才能融会贯通,掌握解决问题的本质要点;你也不会再受限于单一的具体方法,因为使用任何方法都能快速上手和灵活运用,并得到差不多的同等效果。

因此,本文最后这一章节将对各种架构制图方法进行归纳总结,并尝试提炼出一个通用的架构制图方法论,期望能帮助大家更好地理解架构制图背后的原理和思想。即便现在所熟知的各种方法与工具终会过时,也依然能风轻云淡地看待它们的新老交替:**过去是 UML,现在是 C4,未来是什么呢?这并不关键,**因为即使方法过时了,背后的方法论也不会过时。

所以,那些茫茫多的方法背后,究竟是什么样的核心方法论在支撑着呢?经过作者呕心沥血冥思苦想了近 15 秒钟,终于总结出了如下这套经典方法论(p.s:就是凑数的,不要太当真~ )。由于其中包含了 5 个环环相扣的要点,我们姑且称它为:五环理论

1. 理解制图目标

架构制图的第一要点,是需要先深刻理解制图目标。正所谓“以始为终”,有了目标我们才能清晰地前行;否则漫无目的地乱窜,往往会多走不少弯路,甚至南辕北辙。架构制图的目标是什么?其实前文已经提到过很多,这里再简单总结下:

  • 准确(accurate):错的图比没有图还糟糕;即使一开始是准确的,后面也需要定期更新校对;

  • 完整(complete):需要覆盖架构的核心要素和关键信息,为受众呈现一个没有残缺的完整架构设计;

  • 清晰(clear):制图时最好带上图例(形状、颜色、线型、箭头);用图描述不清的地方,还可以加上文字标注做补充;

  • 一致(consistent):比如同一类型的图,最好使用相同的记号风格,以降低受众的理解成本;不一致往往还会带来混淆;

  • 简洁(consise):在满足以上 4 点基础之上,还需要让图更加简洁,一方面是更容易被人接受(没人读 = 没写),另一方面更新维护成本也更低。

2. 找准受众和关注点

架构制图的第二要点,是要找准你制图的受众(audience)以及他们各自的关注点(concern)。找不准的话,要么效果大打折扣(不是他们想听的),要么犹如对牛弹琴(他们根本就听不懂)。常见的一些受众和关注点可包括:

  • 研发:一般会关注很多实现相关细节,比如技术选型、实现可行性、可维护性等,毕竟他们是架构的最直接消费者;

  • 运维:不太关心应用内的具体技术实现(当成黑盒),但很关心各个应用实例的物理部署方式、网络连通性、可运维性等;

  • 安全:只关注系统是否有安全风险,例如是否可能被注入恶意代码、是否有权限漏洞等;如果经历过安全评审,应该很有体感;

  • 产品:大部分情况下只关心项目能否按期上线,其他方面...可能表面上表示些许关心,实际上要么并不在乎要么真的不懂。

3. 自顶向下逐层描述

架构制图的第三要点,是合理运用层次化(hierarchical)的套路,自顶向下逐层描述。无论是 C4 模型还是 arc42 模板,背后都深刻运用并显著强调了这一点。为什么一定要这么做?其中蕴含了两个普适的原理:

  • 分而治之:软件领域中,分而治之是控制和应对复杂系统的最有效方法。而层次化拆分,本质上就是一种分而治之手段:将系统按照从粗到细的粒度,一级一级地拆分成多个相对独立和低耦合的元素(子系统、应用、组件等);

  • 金字塔原理:这本书的核心观点就是,按照自顶向下的方式,先抛出主观点再依次用各个子观点去论证。这样的沟通方式更符合人类的思维逻辑,也更容易让读者接受。简单来说,就是要“先说重点”,帮助读者做归纳总结和划重点,而不是先抛出一大堆细枝末节的零散东西让读者自己去消化和推演。

4. 使用多种架构视图

架构制图的第四要点,是在向传统的工程制图方法论致敬:使用多种架构视图来描述你的架构。在工程制图的世界里,任何立体的制品,大到机床小到零件,都至少需要通过三种视图(主视图、俯视图、左视图)来描述。作为现实世界的映射,软件系统也是多维和立体的,只用单一视图不可能覆盖所有关键的架构信息;即使强行把这些信息都塞在一张图里,那也一定会复杂到让人无法理解。

在架构设计领域,架构视图(architectural view)有专门的定义:针对系统架构某一个方面(aspect)的一种描述;每个视图都会覆盖项目干系人的一种或多种关注点。从上述定义可以看出来,不同的架构视图会有不同的侧重点,同时在描述自己所专注的方面时也会略去与当前视图无关的其他细节 —— 这其实也是一种与层次化拆分类似的分而治之思想,只不过这里是针对完整系统的维度分解,而层次化则是针对某一具体视图再做自顶向下的垂直下钻(drill-down);两者是正交且可以相互配合的,例如前面说到的结构视图、部署视图甚至动态视图,都可以分别再进行层次化拆分。

5. 遵循规范和最佳实践

架构制图的第五要点,其实只是一句正确的废话:遵循规范和最佳实践。这一点已经不限于架构制图,而是上升到了工程实践领域的通用方法论层面。正如前面章节所说,“学习架构制图的目标,就是要把它从一门手艺变成一项工程”,因此架构制图的“施工”过程也理所应当符合工程化思维

  • 一方面,制图需要遵循明确的规范,在理论层面进行约束和指引,确保过程和产物的高质量与标准化;

  • 另一方面,制图还需要遵循业界最佳实践,在实践层面持续吸取优秀经验,不断精进自己和团队的制图技能。

附:架构描述标准化概念模型

国际上对架构描述其实建立了专门的标准(ISO  / IEC / IEEE 42010:2011),其中的很多概念词汇在本文中都有提到(e.g. Stakeholder、Concern、View、Viewpoint),有兴趣的同学可以进一步研究下。