做架构评审这些年,我常在一开始问对方一个问题:“你这张图,想给谁看?”很多人会愣一下,然后开始重新组织语言。UML五大视图,也就是常说的4+1视图模型,本质上就是在回答这个问题:一个系统至少得从5个不同视角去描述,每个视角对应不同的利益干系人、不同的关注点、不同的一组图,把它们拼在一起才算是完整蓝图。这篇笔记适合软件架构师、后端开发者、技术负责人,以及正在学UML或准备系统架构设计考试的学生。读完你会明白每个视图解决什么问题、各自用什么图、视图之间怎么保持一致,以及在实际项目里怎么把这套方法用起来。
1. 为什么是4个视图再加1个场景
1.1 不同角色看到的系统本来就不一样
同一个系统,在用户眼里是一组功能,在架构师眼里是一堆类和接口,在运维眼里是一批进程和服务器,在测试眼里是模块边界和接口契约。谁的视角都不能代表全部,但谁的视角都不能缺。
举个很常见的例子:某订单系统上线后出现支付超时问题。用户看到的是“付款转圈圈”;开发查了半天,发现是订单服务和库存服务之间那个同步调用没有做超时控制;运维一看监控,发现两台应用服务器负载不均,请求全打在老节点上。这里已经涉及了功能、代码结构、并发交互、部署拓扑四个不同层面的问题,单靠一张架构图根本说不清楚。
这就是4+1视图模型的出发点:逻辑视图回答“系统由哪些抽象协作组成”,进程视图回答“运行时如何并发和通信”,开发视图回答“代码怎么组织、模块怎么划分”,物理视图回答“系统部署在哪里、节点之间怎么连”,最后再用一个场景视图把所有东西串起来。
1.2 越复杂的系统,越不能用一张图描述
很多团队习惯画一张“大而全”的架构图,把类、接口、服务、数据库、服务器全堆在一起,结果图越来越满,能看懂的人越来越少。这不是绘图水平问题,而是信息维度太多了。
打个比方:盖一栋房子,需要平面图、立面图、结构图、水电图、效果图,每张图服务于一个施工阶段或一类工程师。如果只给施工队一张效果图,他们完全不知道怎么砌墙布线。软件系统比房子更抽象,它没有物理实体可以“围着看一圈”,只能用不同视角的模型交叉表达。
4+1里那个“4”,说的就是4个正交的基础视图:逻辑视图管静态结构、进程视图管动态行为、开发视图管代码组织、物理视图管部署环境。每张视图都只承载一部分信息,组合起来才不会信息爆炸,也不会丢失关键细节。
1.3 5个视图不是画5张独立的图,而是互相约束的一整套模型
如果只是“画5张图”,那这套模型也没什么特别。真正有价值的是视图之间的映射关系:逻辑视图里的一个类,最终会跑在进程视图的某个线程里,被打包进开发视图的某个组件中,再部署到物理视图的某个节点上。
我见过很多团队,每个视图都画得挺漂亮,但一追问“类A部署在哪台机器上”“订单服务挂了会影响哪些用例”,就答不上来了。这说明视图之间没有建立追踪关系。4+1视图模型要求你在设计每个视图时,都要同时想清楚它和其他视图的接口在哪里,这样任何一处变更,都能顺着链路评估影响范围。
2. 四个基础视图逐一拆解
2.1 逻辑视图:系统的骨架结构
逻辑视图从抽象设计角度描述系统,重点是把职责划分清楚。这里的主角是类、对象、接口、包以及它们之间的关系,常用图包括类图、对象图、状态图和包图。
画逻辑视图时,我习惯先识别三种类型的分析类:实体类、边界类、控制类。实体类负责承载核心数据和业务规则,比如订单、支付记录;边界类负责与外部交互,比如接口控制器、页面模型;控制类负责编排业务逻辑,比如下单流程处理器。用这种思路画出来的类图,层次感会好很多,不会所有类都平铺在一张图上。
以某订单系统为例,逻辑视图至少要有订单聚合、订单项、商品库存、支付单这四类核心对象。它们之间的关系要画清楚:订单和订单项是聚合关系,订单项指向商品,支付单依赖订单。加上操作,类图才真正有用——比如扣库存的方法必须放在商品类上,而不是放在订单服务里,否则后面做并发控制时会很别扭。
这里有个新手特别容易踩的坑:把逻辑视图当成数据库ER图来画。逻辑视图里的类不都对应数据库表,尤其是控制类和边界类,它们只是运行时协作单元。过早纠结字段与表结构,会把架构设计拖回详细设计的泥潭。
2.2 进程视图:并发与协作的控制流
如果说逻辑视图描述的是“系统里有什么”,进程视图描述的就是“它们怎么活起来”。它关注进程、线程、并发、同步、通信、事件调度这些运行时问题,常用图是活动图、时序图和协作图。
很多并发问题在逻辑视图里看不出来。比如两个线程同时调用商品的扣库存方法,逻辑视图上只是“商品类有个方法叫扣库存”,但进程视图里必须明确:这个方法有没有加锁?用乐观锁还是悲观锁?失败了怎么重试?库存扣减和订单创建是同步执行还是异步削峰?
我处理这类问题时,首选活动图加泳道。泳道按角色或系统划分,比如用户、前端系统、订单服务、库存服务。先画出主流程:创建订单、扣库存、支付。然后在扣库存这一步用分叉汇合符号表示“订单服务和库存服务并行处理”,再在汇合点标注等待条件。这样整个流程的控制流非常直观,评审时大家一眼就能看出哪里可能会死锁、哪里需要超时。
时序图则适合表达对象之间的消息顺序。画时序图时,消息顺序就是代码执行顺序,但注意只画关键交互,不要画出所有方法调用。进程视图的目的不是记录代码细节,而是暴露并发风险和数据一致性问题。
2.3 开发视图:代码仓库内的组织秩序
开发视图面向程序员,描述代码如何组织、模块如何划分、构建单元之间如何依赖。常用图是包图和组件图。
包图是逻辑视图类图的下沉细化。逻辑视图里定义了一堆类,开发视图要把这些类归入不同的包,比如控制器层、应用服务层、领域层、基础设施层。划分包时,我遵循两个原则:依赖只能从上往下,不能反向依赖;被依赖多的稳定模块要放底层,不能随意改动。
循环依赖是最常见的头痛问题。A包依赖B包,B包又依赖A包,改一处两边都崩。解决办法是引入更底层的抽象包,把两者共同依赖的接口抽出来,让A和B都依赖这个抽象。画包图时,我会顺手用依赖分析脚本扫一遍代码,看有没有违反依赖规则的边,发现问题马上处理,不然包图很快就和代码脱节。
组件图则负责描述可交付单元,比如一个Java项目里的service-api.jar、service-impl.jar,或者一个微服务项目里的order-service、payment-service。组件图能清楚展示每个组件的对外接口和依赖关系,是开发和运维都能看懂的“中间层”视图。
2.4 物理视图:跑在机器上的真实世界
物理视图描述系统硬件节点、软件部署和节点间的通信路径,核心图是部署图。节点包括应用服务器、数据库服务器、消息队列集群、负载均衡器,甚至网关和CDN。
画部署图不能只画个服务器方块加一条线。每个节点上要标明部署了什么组件或构件,每条通信路径要标注协议和端口,比如HTTP/8080、JDBC/3306。还要注意哪些节点是集群关系、哪些是主备关系、哪些是无状态可水平扩展的。
实操中,物理视图很容易和网络拓扑图混为一谈。网络拓扑画的是路由器、交换机、防火墙,部署图画的是软件组件的分布。两者信息不同,不要互相替代。另外,物理视图必须保持更新,尤其是云原生环境下,容器数量动态变化,部署图不用画到每个Pod粒度,但至少要画出逻辑部署单元和关键的伸缩边界。
3. 场景视图:把所有视图串起来的那根线
3.1 为什么“+1”是场景而不是别的
四个基础视图各讲各的,如果没有人把它们组织起来,架构就会变成一堆静态文件。场景视图承担的就是这个组织角色。
场景是什么?就是用户和系统交互的具体故事。比如“顾客创建订单并支付成功”就是一个场景,它有主成功路径、扩展路径和异常路径。每个场景会在逻辑视图中触发一组对象协作,在进程视图中触发一组并发行为,在开发视图中映射到具体组件,在物理视图中落到具体节点。一篇重要的需求文档如果没有场景,架构师根本不知道从哪下手。
所以场景视图常用用例图来描述系统边界和参与者关系,再配合活动图、时序图细化每一个用例的行为。用例图是入口,活动图和时序图才是场景的实体内容。
3.2 从用例图推导架构的完整链条
我推荐按下面这条路走:先写用户故事或用例描述,再画用例图;从用例中识别参与者、前置条件、后置条件和主流程;接着每个主流程画一张活动图,找出需要协作的对象;再把活动图的步骤转为时序图消息,得到类之间的调用关系;然后根据调用关系确认逻辑视图中需要的类;最后把类归入包和组件,确定并发模型,完成部署设计。
举例:某“创建订单”用例。用例图里参与者是顾客,用例包括创建订单、扣库存、支付。活动图显示:顾客提交订单,系统校验库存,锁定库存,创建订单,跳转支付。转换到时序图时,发现需要OrderController、OrderService、StockService、OrderRepository、PaymentClient这几个对象协作。于是逻辑视图的类图就有了雏形,进程视图也会发现“锁定库存”必须是一个跨进程的原子操作,开发视图里对应一组API和实现模块,物理视图里则是应用服务与数据库服务的交互链路。
这个过程就是4+1模型最核心的用法:场景是需求,四个视图是设计,从场景出发推导设计,再拿场景去验证设计。
3.3 场景视图的常见误区
第一个误区是把用例图画成需求说明书。用例图只表达参与者与用例的关系,真正的行为细节必须通过用例描述或活动图补齐,否则一个“支付”用例放在图上,谁也不知道它背后有多少逻辑。
第二个误区是用例粒度失控。有的团队把“输入手机号”“点击按钮”也画成用例,图上一两百个节点,完全失去架构视角。用例应该是对用户有价值的完整流程,粒度控制在“一个参与者通过系统完成一个目标”这个级别。
第三个误区是只画主成功路径。异常路径和扩展路径里藏着大量系统间协议和容错逻辑,这些才是进程视图和物理视图最需要的内容。画场景时至少把主路径、一条扩展路径、一条异常路径都走一遍。
4. 视图之间如何保持一致
4.1 用追踪矩阵建立视图映射关系
视图一致性不是靠自觉,而是靠可追踪的映射关系。我见过最有效的方法是维护一张小型追踪矩阵,行是一个个关键场景,列是逻辑视图的主要类、进程视图的进程或线程、开发视图的组件、物理视图的节点。
| 关键场景 | 逻辑视图核心协作 | 进程视图处理单元 | 开发视图组件 | 物理视图部署节点 |
|---|---|---|---|---|
| 创建订单 | OrderController / OrderService / OrderRepository / Product | 订单服务进程 + 库存服务进程 | order-service.jar / stock-service.jar | app-1节点、stock-1节点 |
| 支付回调 | PaymentClient / OrderService / PaymentRecord | 支付回调线程池 | payment-client.jar | gateway-1、app-1 |
| 库存预警 | StockMonitor / StockRepository | 定时任务线程 | stock-monitor.jar | batch-1 |
这张矩阵不需要覆盖所有类,只需要覆盖关键业务场景和重大决策点。每次架构评审都拿这张表走一遍,哪一行对不上,就是哪里不同步,要立刻修正。
4.2 视图不同步的典型场景
类图改了,组件图没改。这是最常见的情况。开发在代码里把订单服务和支付服务合并成了一个组件,但组件图还画着两个独立模块,导致运维按旧图部署,发布全乱。解决方式很简单:组件图是交付文档,代码合并后必须同步更新,并走评审。
部署图与真实环境不一致。某项目上线时引入了消息队列做异步削峰,但部署图一直没更新,后来排查性能问题,运维按旧图以为是数据库瓶颈,查了半天才发现消息队列才是关键路径。部署图要跟着基础设施变更走,至少每个迭代更新一次。
进程视图遗漏并发策略。逻辑视图里画了共享的库存对象,进程视图却没有标注任何锁机制。结果压测一上来就超卖。进程视图是并发安全的唯一表达位置,不能让开发自己去代码里猜。
4.3 一致性靠流程、靠工具、靠基线
先靠流程:每个迭代安排一次架构走查,选3到5个核心场景,从用例开始把四个视图依次过一遍,任何不一致当场改。走查不是走过场,必须有架构负责人拍板。
再靠工具:如果团队用文本化建模工具,比如PlantUML或类似方案,视图就是代码,纳入版本库管理,别人改了视图可以通过diff看到变化点,比在绘图工具里改图再截图发群靠谱得多。用桌面建模工具时,也可以利用元素链接功能把类和组件关联,但要看团队是否愿意维护这些关系。
最后靠基线:关键视图要有版本概念,对外发布的架构图必须标记版本号和修订日期。没有基线的架构图,过三个月就没人知道哪张是当前生效版本,一致性问题会失控。
5. 从零开始走一遍5个视图
5.1 一个迷你订单系统的建模全流程
我拿一个演示用的“订单管理系统”举例,这个系统很小,但足够走通流程。
第一步,画场景视图。参与者是顾客和管理员,核心用例是下单、支付、查询订单、取消订单、商品管理。这里下单用例要先写用例描述,前置条件是商品有货、用户已登录,主流程是创建订单、扣库存、生成待支付单,扩展流程是库存不足提示,异常流程是支付超时取消订单。
第二步,画活动图。下单活动图分三个泳道:用户、订单服务、库存服务。主流程执行到“扣库存”时,订单服务和库存服务之间是同步调用,但在高并发版本里改为异步消息。
第三步,画逻辑视图类图,用PlantUML表达如下:
@startuml class Order { -id: Long -createTime: Date -status: String +addItem(item: OrderItem) +calculateTotal(): BigDecimal } class OrderItem { -productId: Long -quantity: int -price: BigDecimal } class Product { -id: Long -stock: int +deductStock(num: int): boolean } class Payment { -orderId: Long -amount: BigDecimal -status: String +pay(): boolean } Order "1" *-- "1..*" OrderItem Order *-- "1" Payment OrderItem --> Product @enduml第四步,画进程视图。这一步把“下单”拆成几个关键线程:接收请求的Web线程池、处理业务的业务线程池、异步处理扣库存消息的MQ消费者线程。扣库存一定要用分布式锁或乐观锁,否则并发下数据会错。
第五步,画开发视图。代码分成order-api、order-core、order-infra三个模块,order-core是核心业务模块,order-api对外暴露接口,order-infra负责数据库和消息队列适配。组件依赖方向是api和infra都依赖core,api不依赖infra,这样替换技术栈时不需要动接口层。
第六步,画物理视图。部署结构是nginx负载均衡到两台应用节点,应用节点连接MySQL主库和本地缓存Redis,MQ集群独立部署。应用节点无状态,可水平扩容,数据库主从同步,从库用于查询报表。
这套流程在真实项目里不会一次走完,通常先画场景和逻辑视图,再根据架构评审结果细化进程和开发视图,物理视图在部署方案确定后补上。关键是顺序不能乱,场景定了结构才能定,结构定了并发和部署才有依据。
5.2 工具选型对比
不同团队用不同工具,我不强推某一个,只分享一下选择时看重的几个维度。
| 工具思路 | 代表工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 文本建模 | PlantUML | 文本即图、版本库可diff、学习成本低、可自动生成 | 复杂布局调整麻烦 | 后端团队、文档走Git审查 |
| 在线白板 | draw.io | 免费、上手快、协作方便 | 图形与代码分离、不好做版本管理 | 方案初稿、头脑风暴 |
| 桌面建模 | StarUML | 轻量、支持多种UML图 | 商用需授权 | 个人学习、小型项目 |
| 企业级建模 | Visual Paradigm | 功能全面、支持需求管理和追踪 | 重、贵、学习曲线陡 | 大型组织、合规审计 |
选型原则很简单:如果团队把架构图当交付物来维护,优先选文本化工具;如果只是评审时用一下,在线白板就够了。不要把工具当成目标,目标是有多少人能看懂并维护。
5.3 微服务与云原生时代,4+1还适用吗
这个问题我经常被问到。4+1模型确实来自传统单体应用时代,但它没有过时,只是使用方式要调整。
逻辑视图在微服务里下沉到单个服务内部,每个服务有自己的领域模型和类图;进程视图上升为服务间通信和分布式事务控制,重点变成消息可靠性、幂等、超时和降级;开发视图对应代码仓库、镜像和CI流水线;物理视图对应容器编排集群、命名空间和节点池。场景视图依然担任入口,只是从“一个系统”变成了“全链路”。
这几个视图里,进程视图的重要性明显提升。微服务架构下,很多问题都出在服务间协作:同步调用链太长、异步消息丢失、分布式事务边界不清。这些必须在进程视图里画清楚,否则上线就是灾难。
另外,业界还有一种更轻量的C4模型,把架构分为上下文、容器、组件、代码四个层级,可以看作4+1在特定条件下的一种务实变体。它没有替代4+1,但有时候更贴近团队习惯,建议按项目实际复杂度去选。
6. 高频问题与避坑经验
6.1 遇到过的现实问题速查
| 现象 | 根因 | 对策 |
|---|---|---|
| 架构图三个月没人看 | 图和代码脱节,维护成本高 | 只维护关键视图,纳入评审流程 |
| 类图又大又乱 | 一个图塞了几十个类 | 按包拆分,一个包一张图 |
| 用例图几百个节点 | 用例粒度到操作级 | 回到“参与者目标”粒度,按模块分组 |
| 并发问题反复出现 | 进程视图没有同步机制 | 每个关键用例必须画活动图和时序图 |
| 部署图和实际不一致 | 基础设施变更没人同步 | 部署图跟随发布迭代,每次发布时核对 |
| 新同学看不懂架构图 | 没有场景贯穿 | 用场景视图做新员工架构入门 |
6.2 几条行之有效的实操心得
第一,先定场景再定结构。没有场景的架构设计容易自嗨,画了一堆类,不知道哪个是核心。先写3到5个关键用例,再从用例里提取类和协作关系,效率高得多。
第二,每个迭代只更新与本次功能相关的视图。不用每次把所有视图都重画一遍。功能只改了订单模块,就只更新订单相关的逻辑视图、时序图以及对应的组件和部署影响,没必要为了更新而更新。
第三,删图比画图更难。视图的价值在评审和信息沟通,不在数量。如果一个视图没有在评审会上被问过问题、没有被用来分析变更影响,那它就是冗余的,大胆删掉。
第四,图要能反向指导代码。我每次画完视图都会做一件事:拿着图去看代码,看实现是否对齐。如果图说A依赖B,代码却是反的,图就是错的。保持图和代码一致,比画图技巧重要一百倍。
最后说一点个人体会。我刚开始用4+1视图时,总想着把每个视图画得又全又细,结果花费大量时间在维护图上,根本没发挥模型的作用。后来改成“场景驱动、按变更更新、评审时走查”的方式,团队对架构讨论的质量明显提升。画图能力是基本功,但真正拉开差距的,是你能否让每一张图都在正确的时间、以正确的粒度、讲给正确的人听。