做系统设计、重构或者技术交接的时候,最怕的就是拿到一个“黑盒”。别人丢过来一套系统、一个产品模块,你第一反应往往是:这东西到底包含哪些部分?各部分之间怎么协作?改动一个点会不会牵连一大片?我在做系统模块拆解这件事上踩过不少坑,也总结了一套可以照抄的作业流程。这篇文章就围绕“第二阶段:写拆解”展开,系统说说怎么把一个黑盒拆成一张清清楚楚的结构图。
可能有人会问,拆解不就是画个架构图吗?真这么简单就好了。模块拆解的本质,是把一个复杂的系统按某种规则切分成边界清晰、职责单一、依赖明确的小单元,然后再把这些小单元的相互关系、数据流转、异常处理全部落到纸面上。这个动作在系统改造、性能优化、团队扩容、甚至新人入职培训里都极其关键。无论你手里是一个运行多年的老系统,还是一个刚立项的新平台,拆解能力都是硬通货。
这篇文章适合谁看?适合刚接手新系统的后端开发、做系统梳理的技术负责人、准备做重构或迁移的架构师,也适合产品经理想搞懂技术边界、项目经理想摸清工作量。我会按我实际做拆解的先后顺序来讲,怎么理边界、怎么画视图、怎么写模块说明、怎么验证拆得对不对,尽量把每一步都讲到可以直接复用的程度。
1. 拆解之前先想明白:模块边界到底怎么划
1.1 拆解不是画图,是回答四个问题
很多人一上来就找画图工具,先把网络拓扑、系统框图画出来,这是本末倒置。模块拆解的核心目标,是先回答几个更底层的问题:
- 这个系统具备哪些能力?每个能力由谁负责?
- 这些能力之间是怎么通信的?同步调用还是异步消息?
- 数据归属权在谁手里?哪些数据是模块私有的,哪些是全局共享的?
- 一个模块挂了,影响范围是什么?另一个模块会不会跟着遭殃?
我在给一个订单系统做拆解的时候,最开始就拿着一张旧架构图开始看,结果发现图上画了一个叫“数据中台”的框,但谁也说不清这个框具体做什么。我只好去翻代码、查日志、问开发,最后才搞清楚,所谓数据中台实际上承担了订单数据同步、用户标签计算、报表统计三件完全不相关的事。这就是边界没划清楚导致的典型问题。
所以在拆解时,我给自己定了条规矩:画图之前,先用文字把问题回答清楚。宁可文字啰嗦一点,也不能上来就用图形去掩盖认知盲区。
1.2 高内聚、低耦合怎么落地到具体模块
“高内聚、低耦合”这句话谁都会说,但真到了划分边界的时候,往往就变成拍脑袋。我总结出三个可以操作的标准:
第一,模块内部的事务逻辑必须完整闭环。比如订单模块,创建订单、查询订单、取消订单、订单状态变更这些事必须能在一个模块里说清楚,不能一个订单状态变化要跨三个模块才能理解。第二,模块对外暴露的接口要尽量少而稳定。如果一个模块对外有几十个接口,首先要想的不是怎么拆小接口,而是这个模块是不是承担了太多职责。第三,模块之间只通过明确的契约通信,不允许直接读别人的数据库表。
我有一个挺直观的类比:好的模块划分就像一栋写字楼里的公司。每家公司有自己独立的办公区,有自己的门禁,公司之间要通过前台预约才能见面沟通。而糟糕的模块划分就像开放式工位,大家坐在一起,看起来热闹,但一个人打个喷嚏,一排人都得跟着受影响。代码上体现出来就是:服务之间绕过接口直接查数据库、共用同一张表、或者一个改动要同时发布好几个服务。
1.3 拆解的粒度怎么才算刚刚好
粒度是模块拆解里最让人纠结的问题。拆太粗,一个模块几千行代码,和没拆没区别;拆太细,一个模块就一个接口,管理成本比开发成本还高,运维想死的心都有。
我个人的经验准则有两条:一是看团队规模,一个模块至少要能由一个团队在一个迭代周期内独立交付,如果两周都交付不了,说明拆得还不够;二是看故障半径,如果一个模块故障会导致核心链路全部瘫痪,那这个模块就该继续拆,直到故障影响范围可接受为止。
举个实际例子。之前拆过一个支付相关的系统,刚开始把所有支付方式(微信、支付宝、银行卡)都塞在一个模块里,每次上新渠道,改动面都特别大。后来我们把渠道适配层和核心交易引擎拆开:交易引擎负责支付状态机和对账,渠道适配层负责对接第三方接口。这样支付引擎稳定,渠道适配可以各自迭代互不影响,虽然多了一个模块部署运维,但整体风险反而降下来了。
2. 拆解前的信息收集:先把自己的认知填满
2.1 先建立全局视图,再动手拆
正式写拆解文档之前,我建议先花一到三天时间做信息收集。因为你拆解的对象如果是老系统,文档可能早就过时了;如果是新系统,可能在演进过程中已经偏离了当初的设想。全局视图这一步,核心目的是回答三个问题:
- 系统上下游是谁?依赖了哪些第三方?被谁依赖?
- 核心链路是什么?比如电商系统,下单、支付、发货就是核心链路,如果这条链路走不通,拆解出来的其他模块做得再漂亮也没意义。
- 哪些是主模块,哪些是支撑模块?主模块承担核心业务逻辑,支撑模块提供通用能力(比如消息推送、权限校验、日志埋点)。
这一步我习惯用最笨但也最有效的方法:把代码仓库拉下来,按服务目录列出清单,再结合线上配置中心确认服务之间调了哪些接口。不用急着弄懂每行代码,但要把服务清单、接口清单、依赖关系清单这三张表拉出来。
2.2 梳理依赖关系,区分强依赖和弱依赖
不是所有依赖都值得同样级别的关注。我在拆解时,会把依赖关系标注成强依赖与弱依赖两类。强依赖指上游挂了,下游完全无法提供服务;弱依赖指上游异常时,下游可以用降级方案继续跑,最多损失一些扩展功能。
把这两类分清楚,最大的价值是判断故障影响半径。比如用户模块挂了,订单模块还能不能创建订单?如果订单模块的“下单”动作强依赖用户模块做实名校验,那用户模块故障,订单链路就断;如果只是在下单后异步给用户模块发一条消息,那下单链路就不会被用户模块拖垮。
我把这个结果整理成一张依赖矩阵放在拆解文档的最前面,后面再细讲各个模块,看的人脑子始终不会乱。
2.3 摸清数据归属,别让数据变成糊涂账
模块拆解里最容易漏掉的是数据归属。很多系统的代码模块划得很清楚,但数据库表共用得很随意:订单模块要查用户信息,直接去读用户库的表;库存模块要判断订单状态,直接去订单库里查。这种操作在短期看效率高,长期看就是定时炸弹。
拆解时,我要求每个模块说明自己“拥有”哪些数据、哪些数据是复制过来读的、哪些数据是别人拥有但自己需要依赖的。一个简单易行的方法是:把数据库里的核心表列出来,标注它们的属主模块和消费模块。属主负责数据正确性,消费方只能通过接口或明确的消息去读。一开始这么标的时候会发现很多表都是“小区公共花园”,谁的都不是,谁都敢改,这种表通常就是要重点治理的隐患。
3. 写拆解的落地姿势:清单、模板与命名规范
3.1 用一套维度化模板描述模块,而不是写散文
拆解文档最忌讳写成大段大段的描述性文字。二三十个模块,每个模块写五百字,合起来一万多字,看起来很多,但几乎没法检索,也没法横向对比。我推荐的模块描述模板是“字段化”,每个模块用一张表来说明:
| 字段 | 说明 | 示例 |
|---|---|---|
| 模块名称 | 统一命名,团队内唯一 | 订单服务 |
| 模块职责 | 一段话说清楚这个模块做什么 | 负责订单创建、查询、状态流转与超时关闭 |
| 核心功能点 | 按功能粒度列出 | 创建订单、取消订单、订单状态查询、超时自动关闭 |
| 依赖模块 | 运行时依赖哪些模块(区分强弱) | 强依赖:用户服务;弱依赖:库存服务 |
| 被谁依赖 | 反向依赖 | 被支付回调服务依赖 |
| 数据归属 | 拥有哪些表/数据 | 订单主表、订单明细表、订单状态变更流水表 |
| 对外接口 | 核心接口清单 | /api/order/create、/api/order/query、/api/order/cancel |
| 关键流程 | 内部核心链路说明 | 下单状态机:待支付→已支付→已发货→已完成;取消:待支付→已取消 |
| 故障影响 | 模块异常影响范围 | 订单模块不可用则用户无法下单,支付回调无法更新订单状态 |
| 变更频率 | 近半年平均改动频率 | 平均每两周一个版本,主要集中在渠道适配 |
用字段化模板的好处,一是写的人不容易漏信息,二是读的人可以做到“只看一列”快速对比多个模块。我在团队里推行这套模板后,新人上手老系统的速度明显快了一截,因为他们终于不用从代码里猜业务边界了。
3.2 模块命名规范:命名混乱是拆解失败的信号
模块命名的混乱程度,往往和系统健康程度成正比。有的模块叫“核心交易”、有的叫“交易服务”、有的叫“trade-api”,本质上说的是同一个东西,开会的时候互相听不懂,浪费时间不说,还容易在排障时找错对象。
我在拆解时会把命名规则一并定下来。规则很简单:业务域 + 功能角色,例如“order-api”“order-core”“inventory-worker”“user-query”。同步接口统一带 -api 后缀,异步任务统一带 -worker 后缀,核心逻辑统一带 -core 后缀。这样一看名字就能判断它属于哪一层、面向谁、承担什么角色。
每次拆解文档做完,我都会做一次命名对照核查:把所有别名、历史遗留叫法列成一张对照表,哪怕代码里没改,文档里也必须用标准名。等到下一次重构时,顺便把代码包名、服务名一起统一,这个债迟早要还,越早还损失越小。
3.3 拆解文档的结构怎么排
我一般把拆解文档分成五章,顺序固定,大家翻起来不费劲:
第一章是总体说明,包括系统定位、全局架构图(文字版或图版)、拆解范围、涉及团队;第二章是模块清单,按核心模块到支撑模块排序,每个模块用上面那张模板表描述;第三章是依赖关系,包括依赖矩阵和关键链路说明;第四章是数据归属,列出核心数据表、属主和消费方;第五章是风险与待确认项,记录还没搞清楚的边角信息。
最容易被忽略的是第五章。拆解过程中总会碰到一些暂时无法确认的信息,比如某个老接口是否有调用方、某张表的写入方到底是谁。千万不要把疑问憋着,全部记到风险清单里,后续再逐个求证。我在实际项目里,很多影响判断的关键信息都是在风险清单里沉淀出来,然后在后续阶段集中解决的。
4. 三种好用的模块视图:拆分结构、调用关系、状态流转
4.1 结构视图:看清模块的归属关系
视图的核心价值,是让人一眼抓住重点。我通常画三类视图。
第一类是结构视图,反映模块之间的层级和组织关系。拿电商系统举例,从上到下可以分成用户触达层(App、H5、小程序)、业务服务层(用户服务、商品服务、订单服务、支付服务)、基础支撑层(消息服务、文件服务、权限服务)。这张图不用画得太细,重点是让人知道系统分几层,每层上有哪些模块。
画出这张图之后,拆解粒度是否合理就很容易看出来:如果某一层放了十几个模块,而另一层只有孤零零一个,那大概率是划分不平衡,需要进行合并或拆分。
4.2 依赖视图:看清调用方向与故障传导路径
第二类是依赖视图,重点表达“谁调谁”。我这里强调“方向”,是因为依赖关系和调用关系的方向往往相反。从数据流看,订单服务为了查询用户信息,可能调用用户服务;但从故障传导看,用户服务故障会传导到订单服务。如果把这两个方向搞混,排障的时候就会走很多弯路。
画依赖视图我有个原则:只画模块级别的依赖,不画接口级别的依赖。因为模块太多了,画到接口级别,图基本没法看。接口细节放到模块说明表里,由每个模块自己负责。依赖视图上我会用线条粗细表示调用频率,用颜色或标签区分强依赖与弱依赖,这样“核心链路在哪”“哪里最容易出事”一眼便知。
4.3 状态视图:看清模块内部生命周期
第三类是状态视图,用于表达模块内部的关键状态流转。不是所有模块都需要画状态图,但核心业务模块必须有。订单模块至少要画清楚订单状态的合法流转路径:已创建能不能直接跳到已完成?已取消还能不能改成已支付?如果拆解阶段不把这些状态机理清楚,后面写代码的时候就会到处埋雷。
我用文字加箭头的方式把状态流转列出来,同时在旁边标注触发条件和异常分支。比如订单支付超时,状态从“待支付”变为“已关闭”,触发条件是“超过支付有效期且未收到支付回调”。异常分支则会额外记录:支付回调与本地订单状态不一致时怎么处理,是对账修正还是人工介入。这些细节才是模块拆解真正值钱的地方。
5. 拆解完不能直接交差:验证和沉淀
5.1 自查清单:用这七个问题印证拆解质量
每次写完拆解文档初稿,我会按下面的清单过一遍。如果有任何一条不过关,就说明拆解工作还没做完:
- 每个模块能不能用一句话说清楚职责?如果说不清,说明模块划分有问题。
- 模块之间的依赖能不能在图上闭环?有没有循环依赖?
- 核心链路是否完整覆盖?从用户发起请求到最终数据落库,能不能讲清楚经过哪些模块?
- 数据归属是否明确?每张核心表有没有确定属主?
- 有无隐藏的共享逻辑?比如多个模块都去查同一个数据源,只是实现不同?
- 异常和降级路径是否被覆盖?不能只画主流程,优雅降级、失败重试、补偿事务都要有交代。
- 排查故障的人能否靠这份文档定位问题?找一个不熟悉系统的同学,让他根据文档回答“订单支付失败应该先看哪个模块”,能快速给出答案,才算合格。
5.2 找三类人做交叉验证
文档自己在写的时候容易陷入“已知视角”,所以我会找三类人做交叉验证:第一类是系统里待得最久的老员工,他可能会告诉你“有个模块实际已经没人维护了”或者“某张表早就没有写入方了,可以并入另一个模块”;第二类是最近接手的新人,他能告诉你文档哪里读不懂、哪里解释不清楚;第三类是下游依赖方,因为真正的边界问题往往出现在模块与模块的夹缝里,依赖方对“交付物长什么样”最有发言权。
在一次拆解订单域的项目中,我本来按代码目录把“订单管理后台”划分成了一个模块,但老员工说这里面有一部分数据来自人工导入,而且导入逻辑和订单主链路没有任何关系。后来我把这部分数据单独拆成“人工辅助数据维护”模块,文档才算是真正贴合实际。
5.3 拆解过程中的“待确认”清单不要丢
很多拆解文档最后会留一堆没法确认的问题,但最怕的就是这些问题在文档里消失。我自己有一个“待确认清单”的固定格式,记录问题描述、涉及模块、影响判断、找谁确认。最终可能有一部分问题短期无解,但下一个接手的人至少知道这些坑存在,不用重新踩一遍。
在拆解文档的“风险与待确认项”里,每一个条目我会同时写清楚它影响的是拆分粒度还是数据归属。比如“订单超时关闭任务目前部署在订单服务内部”这一条,会影响后续是否把定时任务独立拆成一个 worker 的判断。这种信息看起来零碎,其实价值极高。
6. 从拆解到实践:拆解结果必须能拿来用
6.1 拆解结果直接用于排障:一次实战中的验证
有一次线上反馈“订单支付成功但页面还是待支付”,排障的同事第一时间打开我整理的拆解文档,按依赖视图锁定“支付回调 → 订单状态更新”这条链路,再按模块说明表里的对外接口找到对应的回调接口,十个字节如上的检查点逐步排查,不到二十分钟就定位到是回调里幂等校验逻辑的问题。如果换作没有拆解文档的时候,大概率是两三个后端围着一堆日志猜来猜去。
这件事让我更深地意识到:拆解文档不是坐在那儿写给别人看的摆设,它是真的拿来救命的。所以现在我做拆解,一定会要求排障相关的内容落得足够细:接口名、错误码、日志关键字、配置项位置,都要尽量写清楚。
6.2 拆解结果驱动重构优先级
拆解文档还能用来当重构的作战地图。因为你通过依赖视图和数据归属视图能一眼看出哪些地方是“改一处要动一片”的重灾区,这些就是重构的首选对象。
比如我在拆解一个商品系统时,发现商品主数据的写入方竟然有五个模块,其中两个模块只是在商品创建后顺手更新了某个字段。这种情况在拆解前很难被发现,拆解后清清楚楚。我据此重新梳理了数据写入权限,把字段更新收敛到拥有方,另外四个模块改成消息订阅或接口调用,改动风险大幅下降。没有模块拆解,这轮重构基本就是盲人摸象。
6.3 把拆解文档变成持续更新的“活文档”
拆解最怕的是做完就完了,代码一迭代,文档迅速腐烂。我现在的习惯是:把拆解文档放进团队的代码仓库,和项目代码一同评审、一同发布。模块说明表有变化,必须在同一个 merge request 里更新对应文档。交付标准就是“文档不更新,代码不让合”。
另外,模块拆解不是一次性的。“第二阶段”之后还会有第三阶段、第四阶段,随着系统演进,原来看似合理的边界可能变得不合理,当初清晰的模块可能开始腐化。我建议每半年做一次模块拆解的审视:拉一下模块清单,看有没有变了职责但名字没改的,有没有已经空转但没人敢动的,有没有数据归属已经漂移的。把拆解当成一件持续投入的事,系统才能一直保持健康。
7. 写在最后:一点个人的实操体会
模块拆解这件事,看起来是个技术活,实际上更考验细致和耐心。做得好的拆解,能让团队在讨论问题时有了共同语言,能让新人在几天内就理解跑了几年的系统,能在故障发生时少烧几根头发。做得不好的拆解,文档写完就进回收站,下次重构依然抓瞎,所谓系统梳理,最后只是自我安慰。
我个人这几年做下来,最大的体会就是:拆解的价值不在文档有多厚,而在边界是否真的清楚、依赖是否真的画对、数据归属是否真的明确。与其把所有细节都堆在文档里,不如先保证核心链路闭环,再慢慢补充边角信息。一个读起来顺畅、能经得起追问的模块拆解文档,远比一个看起来全面但注水的文档有用得多。
最后再分享一个小技巧:做完拆解,把文档发给团队里一位完全没参与的系统同事,请他只看文档回答“某某功能从哪里改起”。如果他答得出来,这份拆解就过关了;如果他反应半天,说明你还欠着技术债,需要继续打磨。用这个方式验证拆解结果,比你自己反复读十遍都管用。