
做系统设计的人应该都有过这种体验业务文档写了一堆流程图画了好几版规则也梳理得清清楚楚结果到了技术评审的时候别人问一句“所以到底要部署几个服务服务之间怎么通信”一下子就卡住了。业务视角的“原理”和工程视角的“可部署产物”之间始终隔着一层没有捅破的纸。这层纸就是应用视图Application View要解决的事情。它回答的不是“业务怎么运转”而是“业务的运转规则如何被封装成一个个独立、自治、可部署的处理单元Processing Unit”每个单元负责什么职责、对外暴露什么边界、和别的单元如何协作全部在应用视图里落地成型。换句话说业务分析告诉你系统“做什么”应用视图告诉你系统“部署出来长什么样”。这篇文章适合正在从“画图阶段”向“落地阶段”过渡的团队——不管你是刚做完事件风暴不知道怎么往下走还是代码写了一堆但模块边界越来越模糊都可以用应用视图的思路把方案重新收敛一遍。这篇文章我会用一套完整的案例讲清楚应用视图的定位、识别处理单元的方法、封装的边界设计以及从处理单元到最终部署形态的映射过程。1. 应用视图在架构全景中的位置问题空间与方案空间的转译层1.1 为什么“原理”不能直接部署先说一个很常见也很要命的情况。很多团队做完需求分析之后产出的东西是一堆业务流程图、用例描述、规则列表术语全部是业务侧的——“下单”“拆单”“接单”“履约”“结算”。这些内容描述的是“问题空间”业务想达到什么样的目标现实里有哪些约束规则之间怎么关联。问题在于问题空间的产物没法直接部署。你不能把一个“下单流程”扔到服务器上跑起来你只能说“我要部署一个订单服务它接收创建订单的请求处理库存校验、金额计算然后发一条消息出去”。把业务原理转译为具备运行形态的技术组件这个过程就是应用视图的核心使命。从架构全景上看应用视图处在中间层往上承接领域分析的结果往下指导技术实现。它不像领域模型那样关心业务规则的精细化表达也不像部署架构那样关心物理机、容器、网络策略它关注的是——系统由哪些处理单元组成每个处理单元的职责边界在哪里单元之间通过什么机制协作。我见过不少团队跳过这一层直接从业务流程图跳到建表、写接口清单。结果就是业务规则散落在各个服务里一个完整流程被硬生生切到多个服务里却没人说得清为什么这么切后期每次加需求都要跨好几个服务改代码。这就是典型的“原理没有先封装成单元直接散装进了代码”。1.2 处理单元的三个硬性特征应用视图里的“处理单元”不是随随便便把代码归个类就能叫的。它必须同时满足三个特征缺一个都会在后面暴雷自治单元拥有完成自身职责所需的所有信息与资源不依赖直接访问其他单元的内部状态才能工作。它可以依赖别人提供的服务但不能依赖别人替它做完它该做的判断。边界清晰外部只能通过明确暴露的接口命令、查询、事件与它交互内部怎么实现、怎么存数据外部一概不感知。可独立演进在不影响其他单元的前提下一个单元的内部实现可以被替换、升级、甚至整体重写。这是“封装”最核心的红利——把变化关在笼子里。打个不太严谨但很好懂的比方应用视图里的处理单元类似于硬件设计里的芯片封装。芯片内部是几千亿个晶体管怎么布线、怎么设计逻辑那是内部的事外部只关心一件事——引脚定义。引脚是什么信号、什么电平、什么时序对齐了就能插到系统里工作。**处理单元的接口契约就是“引脚定义”内部实现就是芯片Die里的电路。**这就是“封装”这个词在软件架构语境下的真实含义。1.3 业务流程图与应用视图的差别很多团队误以为把业务流程图里的每个泳道换成“服务”就是应用视图了。实际上差得很远。这里用一张表来对照两者的关注点差异维度业务流程图应用视图核心对象业务活动、参与角色、业务规则处理单元、接口契约、协作机制描述语言业务术语技术边界语言API、事件、数据回答的问题业务如何运转系统如何被构造与部署变化来源业务需求变化技术演进、组织变化、非功能需求产物形态流程图、规则表、用例单元清单、接口契约、依赖关系结论很明确先有业务流程图是好事但绝不能直接拿它当架构设计用。应用视图是在业务分析成果之上加入“可部署性”“自治性”“边界性”这些工程维度之后重新组织出来的产物。2. 识别候选处理单元从业务事件与边界倒推自治模块这一章是实操的第一步怎么从一团业务需求里找出那些应该被封装成处理单元的东西。很多人卡在这里原因很简单——不知道从哪里切。其实方法很成熟用的还是事件风暴Event Storming和限界上下文Bounded Context那一套思想只不过落点从“找领域对象”变成了“找处理单元”。2.1 从业务事件倒推责任单元事件风暴里有一个重要产出物叫“业务事件”比如“订单已创建”“库存已扣减”“支付已完成”“货物已发货”。每个事件代表业务里一个已经发生的事实。做应用视图时这些事件就是最靠谱的切入点——每个业务事件背后一定有一个处理单元对它负责要么是这个单元产生事件要么是这个单元消费事件。具体做法是三步走把所有关键业务事件按照时间顺序摆在时间轴上形成端到端的业务流程。对每个事件问两个问题谁的状态发生了变化才产生这个事件这个事件产生之后谁需要知道并做下一件事把回答合并归类——承担同一组职责、处理同一组事件的就圈定为一个候选处理单元。以订单履约系统为例。业务流程里依次会出现订单已提交 → 库存已预占 → 支付已完成 → 订单已审核 → 发货单已生成 → 货物已出库 → 订单已完成。对着这串事件去归责你会发现“订单已提交”“订单已审核”“订单已完成”这些事件围绕的是订单本身状态的流转——订单处理单元。“库存已预占”“库存已扣减”“库存已释放”围绕的是库存数量的计算和变动——库存处理单元。“发货单已生成”“货物已出库”围绕的是履约执行的推进——履约处理单元。这个过程做完一群候选处理单元就浮出水面了。它们的边界天然对应着事件与责任的内聚点而不是随意的代码分包。2.2 用自治边界做二次校验事件倒推出来的单元只是“候选”还要过一遍自治性校验。最简单有效的校验办法是**把单元看作一个黑盒检查它的每项职责变更时需要同步改动其他单元的概率有多大。**如果两个候选单元的职责总是同频变化——比如“价格计算”和“折扣计算”总是同时改那你最好把它们合并成一个处理单元反过来如果两个职责虽然经常出现在同一个流程里但变化节奏完全不同——比如“订单状态流转”和“物流轨迹同步”那就应该拆开。这里有一个判断清单是我在项目里实际用过、觉得很顺手的这个单元的输入输出是否明确可列举如果说不清输入输出边界就是模糊的。这个单元能不能独立运行、独立测试如果不能说明它隐含依赖了其他单元的内部状态。这个单元的数据存储是否完全私有如果有其他单元直接读它的表边界就被打破了。这个单元的修改是否能控制在合理范围内改动经常跨单元扩散就说明切分点选错了。用清单过一遍拿不准的地方就放一放让团队里不同角色的人各自独立过一遍再讨论通常能逼出不少边界问题。2.3 典型错误按表拆、按人拆、按页面拆识别处理单元最大的坑就是拿着非架构维度的参照物去切。我见到过三类很典型的问题按数据库表拆把每张表对应一个“服务”导致一个处理单元只有一组增删改查业务规则根本无处安放单元之间全是碎调用。这是把存储视角当成了架构视角。按团队组织拆前端组一个单元、后端组一个单元、一个开发一个人一个单元。组织架构方便了但单元边界跟着人走的后果是人一变动架构就碎了。按页面拆用户看到什么页面就拆什么服务。页面只是表现层的切片处理单元要考虑的是业务职责的内聚两者根本不是一回事。这些错误有个共同特点——用“看得见的东西”当切分依据。而应用视图要求你切分时眼睛盯着的是“职责”和“变化”这些都是看不见、但架构上真正要紧的东西。3. 封装的边界契约命令、查询、事件与数据私有权处理单元画出来后接下来最重要的工作就是“封装”——把单元内部的实现细节藏起来在外面定义一套清晰的接口契约。这套契约设计得好不好直接决定后面的开发、测试、联调、部署顺不顺畅。3.1 三类接口契约命令、查询、事件处理单元对外的接口契约不外乎三种类型命令Command要求单元执行一个会改变状态的操作。命令通常使用动词命名——“创建订单”“取消订单”“预占库存”。调用方发出命令之后关注的是结果成功还是失败失败原因是什么。查询Query向单元请求数据不应该改变任何状态。查询关注的是返回结构给什么样的数据、以什么粒度返回、是否分页。事件Event单元向外宣告“已经发生了什么”。事件使用过去时命名——“订单已支付”“库存已扣减”。事件发出后发出方不关心谁会消费消费方也不直接“调用”发事件方两边是通过消息机制解耦的。这三类契约混在一起是新手最容易犯的错。比如让外部直接调用一个“查询库存并预占库存”的接口命令和查询混在一个方法里带来的问题就是调用方搞不清楚这个操作有没有副作用、能不能安全重试、怎么排查问题。CQRS思想里的命令查询分离在应用视图的契约设计上同样成立。3.2 契约设计的实操要点每个处理单元在定义契约时有几个细节值得较真命令必须幂等。网络抖动、调用超时、消费方重试这些在生产环境里都是常态。如果同一个“创建订单”命令因为重试被执行了两次后果就是重复订单。所以每个命令都要携带幂等键Idempotency Key比如请求ID或业务订单号处理单元通过幂等键去重。这是最容易忽略、但上线后最救命的细节。查询不暴露内部实体。很多团队图省事直接把数据库实体类作为查询接口的返回对象。短时间内确实快但实体结构一变所有调用方都要跟着做回归测试。正确做法是定义专门的DTOData Transfer Object按调用方的需要裁剪字段。多写几个类换来的是接口结构的稳定。事件要带版本号。消息队列里的坏消息不是“报错”而是“所有消费者都不知道这个事件结构变了”。给事件定义结构版本比如在事件头带一个schema_version字段生产者新增字段时向后兼容消费方按版本做解析好过出了生产事故再到处查谁改了什么。下面是一张我在设计阶段常用的“契约清单”每个处理单元评审前都对着打一遍勾检查项要求通过标准命令命名动词 业务对象语义清晰调用方无需看文档也能猜对用途幂等设计每条命令支持幂等键同一条命令重放不会产生副作用查询隔离返回对象为专用DTO不暴露内部实体结构事件版本事件头带版本号新老消费者可共存错误语义错误码区分业务错误与技术错误调用方可以判断是否应该重试3.3 数据私有权封装的红线应用视图的“封装”里有一条绝对不能破的红线——数据私有权。每一个处理单元必须拥有并管理属于自己的数据存储其他单元要拿数据只能通过查询接口绝对不能直连数据库。打破数据私有权最常见的两个场景一个是报表场景报表系统为了查询方便直连了业务库另一个是联调场景排查问题时DBA直接跑到别的库里去查数据。短期看确实方便长期看代价就是业务迁移时不敢动表结构因为不知道谁在背后偷偷读这些表改了存储引擎也不敢同样是因为未知的依赖。守住这条红线的方法不是靠人自觉而是靠工程手段数据库账号按最小权限分配、报表走独立的只读副本、跨单元数据需求一律通过查询接口走。麻烦是麻烦一点但每段麻烦都在避免未来更贵的重构账单。4. 从处理单元到部署形态单体、模块化与微服务的取舍处理单元是逻辑概念它回答的是“系统由哪些职责组成”。但“处理单元”和“部署单元”进程、容器、服务实例并不是一一对应的关系。把逻辑边界映射到物理部署形态这一步做得好不好直接影响到系统的复杂度上限和团队的交付效率。4.1 处理单元与部署并非天然一一对应这一步有两层意思需要分开看第一层多个处理单元可以部署在同一个进程里。单体架构就是典型——订单处理、库存处理、履约处理全部在同一个应用里跑靠代码级模块边界隔离。这时候处理单元仍然是架构上的主体只不过它们共享同一个进程生命周期、同一个部署管道。第二层一个处理单元也可以部署成多个实例。横向扩容时同一个订单处理单元会启多个实例前面挂负载均衡或通过消息队列分发。这时候部署形态的“多个进程”和架构上的“一个单元”又不矛盾了。所以核心问题是什么时候该把处理单元合并到一个部署单元里什么时候该拆成独立部署单元4.2 三种部署形态的演进路径我在多个项目里实践下来比较稳的路径是单体 → 模块化单体 → 微服务按需演进不要跳级。单体Monolith所有处理单元用一个代码库、一个进程部署。它的优势是开发调试简单、运维负担低、调用的网络开销为零。适合团队规模小、业务复杂度还撑不起分布式成本的时候。缺点不用多说边界全靠自觉代码量大了以后模块之间会互相渗透。模块化单体Modular Monolith代码层面仍然是一个应用但模块边界硬性规定模块之间只能通过明确定义的接口交互不能直接访问彼此的类和数据表。这是我最推荐大多数团队停留的形态——它保留了单体的简单部署又通过强制模块边界保住了处理单元的封装性后期要拆微服务时每个模块也就是现成的拆分候选。微服务Microservices处理单元真正独立部署、独立伸缩、独立故障隔离。它解决的问题是规模与组织复杂度——当团队大到一定规模、模块之间必须独立发布和扩容时微服务的收益才会盖过它的成本。这张表是我给团队做部署形态选型时用的决策参考直接贴出来供参考决策因素倾向单体/模块化单体倾向微服务团队规模12个小组几十人以内多个独立团队各自负责一块业务变更频率低频、节奏统一各模块节奏差异大需要独立发布故障影响可接受全应用一起发布需要故障隔离爆炸半径要小伸缩性需求整体伸缩即可热点模块需要独立弹性伸缩已有技术栈统一简单允许异构技术栈4.3 模块化单体的关键实践如果你决定先走模块化单体有三个实践必须落实否则模块化就是个摆设第一模块之间的访问只能通过公开接口。最好在代码评审阶段就卡住——不允许跨模块直接依赖内部类接口定义统一放在接口层模块实现只依赖接口不依赖别的模块的实现类。第二数据库层面也要隔离。模块化单体最常见的问题是代码层面做了模块隔离但数据库还是公共的一大坨所有模块的表混在一起。要真正为未来可能的拆分做铺垫至少要做到schema按模块隔离每个模块只能访问自己schema里的表。第三给每个处理单元建立独立的构建产物。模块化单体虽然最终一起打包部署但每个处理单元可以单独编译成jar包或者npm包版本独立管理。这样后续拆微服务时构建与发布管线已经提前备好了。5. 从处理单元到实际落地依赖治理、事务边界与可观测性最后聊几个真正落地到生产环境才会暴露出来的问题。画图阶段一切都很干净一上线就开始出幺蛾子处理单元之间的依赖纠葛、跨单元操作的数据一致性、链路日志查不清……这些坑我没少见提前避开能让你的应用视图不只是一张好看的图。5.1 依赖方向绝对不允许出现环路处理单元之间的依赖关系必须是单向无环的。A依赖BB依赖C可以如果A依赖B、B又依赖A那这两个单元的边界定义一定有问题。环路的危害在单体时代可能还不明显到了微服务阶段就是灾难A调B、B调A其中任何一个单元抖动都可能引起循环重试把整个系统打瘫。处理环路的通用方案是引入事件——把双向依赖改成单向调用加异步事件。比如订单单元需要知道库存状态、库存单元也需要知道订单状态与其让两边互相调接口不如让库存单元在“库存已扣减”时发事件订单单元订阅这个事件自己更新状态依赖就从双向变成了单向。5.2 跨处理单元的事务用流程编排而不是分布式事务应用视图里一个很重要的原则一个业务事务不应该横跨多个处理单元去共享一个数据库事务。扣库存和创建订单如果在一个事务里就意味着它们的数据必须放在一个库里这与处理单元的自治性是冲突的。解决跨单元一致性的落地思路是Saga模式。以订单流程为例订单单元创建订单发事件 → 库存单元扣减库存如果不够发CounterOffer或者抛异常 → 如果扣减失败通过补偿动作把订单取消释放之前预占的一切资源。每个单元只管自己那一步的事务全局的一致性靠编排Orchestration或 choreography事件链完成。这种设计的核心变化是从“强一致”转为“最终一致”。业务上要有容忍短暂不一致的机制——比如订单状态在某个窗口期显示“处理中”而不是直接显示成功。把这个预期告诉产品方是架构落地之前必须对齐的事情。5.3 可观测性从第一天就戴上处理单元拆分后最直接的代价是排查问题变难了——一次请求可能跨好几个单元。所以可观测性不能等上线后再补必须在动手写代码时就纳入设计。每个处理单元至少要做到三件事全链路Trace传递调用入口先生成trace_id通过请求头和消息头一路传到所有下游单元让一次业务的完整调用链可以被串联查询。没有trace_id的系统在微服务阶段基本等于裸奔。结构化日志日志里带上单元名、处理的关键业务对象ID订单号、用户ID、trace_id。这样排查问题的时候可以按业务ID去聚合所有相关日志。健康检查要反映真实可用性健康检查不只要回答“进程活着吗”还应包含关键依赖的连通性——比如数据库是否能连、消息队列是否能生产。不然负载均衡还在向一个已经“半死”的实例转发流量用户侧的错误率根本降不下来。这些设计都不复杂复杂度在于“坚持”。每写一个处理单元都要把这几个点作为标配写进去而不是等出了线上故障再补。我在实际项目里最深的体会是架构图上多花一分钟想清楚依赖方向比上线后花一晚上排查循环调用痛快得多接口契约里多花十分钟做幂等设计比被用户投诉重复扣款之后修复爽快得多。重看应用视图这件工作它的核心价值从来不是画一张漂亮的架构图而是把那些停留在文档里、PPT里的“原理”变成真正能在环境里部署、能够在生产环境里持续演化的处理单元。原则清晰了方法对了后面的一切只是执行问题。