3、服务拆分方法论:基于DDD进行业务域拆分
说到微服务拆分,很多团队一上来就按功能模块切。比如把OA拆成「用户服务」、「流程服务」、「文档服务」……听起来挺合理,对吧?
但实际跑起来就发现问题了。我见过一个项目,拆了十几个服务,结果每个服务里都塞了一大堆业务逻辑,互相调来调去。说白了,就是换了个部署方式,业务耦合一点没少。
为什么会这样?因为拆分没找对「边界」。今天我就用DDD的思路,拿OA里三个典型业务域——审批流、考勤、公文——来聊聊到底该怎么拆。
3.1 核心原则:先找「限界上下文」
DDD里最核心的概念,就是限界上下文。你可以把它理解成一个「业务边界」。边界内的模型、术语、规则是自洽的,出了边界就得通过接口交互。
我习惯这样判断:两个业务概念,如果它们的「生命周期」和「变化原因」不一样,就应该分到不同的上下文里。
举个例子:
「审批流」里的「审批节点」和「考勤」里的「请假单」,虽然都涉及「审批」这个动作,但它们的生命周期完全不同。
- 审批节点:跟着流程模板走,模板变了节点就变
- 请假单:跟着员工考勤周期走,月底结算完就归档
所以,它们应该属于两个不同的限界上下文。
3.2 审批流:以「流程模板」为核心
审批流这个域,核心实体是什么?不是「审批单」,而是流程模板。
你想想看,OA里审批流最头疼的是什么?是流程经常变。今天部门经理审批,明天改成总监审批,后天又加了个会签节点。如果代码里写死了节点顺序,每次改流程都得发版。
我在项目中遇到过这种情况:一个客户要求「请假超过3天需要总经理审批」,我们直接在代码里写了个if判断。结果过了两周,客户说改成「超过5天」……嗯,改代码、测试、上线,折腾半天。
我的建议:
把流程模板设计成「可配置的」。用DSL或者JSON描述节点、条件、路由规则。这样流程变了,改配置就行,不用动代码。
拆分时,审批流域应该包含这些聚合:
- 流程模板聚合:模板ID、节点列表、路由规则
- 流程实例聚合:实例ID、模板ID、当前节点、状态
- 审批记录聚合:审批人、审批意见、时间戳
注意,审批记录不要和流程实例混在一起。为什么?因为审批记录是「历史数据」,只增不改;而流程实例是「当前状态」,经常更新。混在一起会导致锁竞争和性能问题。
3.3 考勤:以「考勤周期」为边界
考勤这个域,核心是考勤周期。每个月一个周期,周期内记录员工的打卡、请假、加班、出差等数据,月底结算。
这里有个容易踩的坑:把「请假」直接放到考勤域里。我见过不少系统这么干,结果请假审批和考勤结算耦合在一起,改一个就得改另一个。
我曾经踩过的坑:
一开始把「请假单」放在考勤域,结果审批流那边要改请假流程,考勤域也得跟着改。后来我把「请假」拆成两个部分:
- 审批流域负责「请假审批流程」
- 考勤域只记录「请假结果」(已审批通过的请假天数)
两个域通过事件(Event)通信。审批流域发布「请假审批通过」事件,考勤域订阅后更新考勤数据。这样两边互不干扰。
考勤域的聚合设计:
- 考勤周期聚合:周期ID、开始日期、结束日期、状态
- 员工考勤聚合:员工ID、周期ID、打卡记录、请假天数、加班时长
- 考勤规则聚合:迟到阈值、旷工定义、加班计算方式
这里要注意,考勤规则是独立变化的。不同部门、不同岗位的规则可能不一样。把它单独拆出来,方便后续做规则引擎。
3.4 公文:以「文档版本」为核心
公文这个域,和审批流、考勤都不一样。它的核心是文档版本管理。
一份公文从起草、审核、签发到归档,中间可能改几十个版本。每个版本都要保留,还要能追溯谁改了哪里。
我建议把公文域拆成这几个聚合:
- 公文文档聚合:文档ID、标题、文号、当前版本
- 文档版本聚合:版本ID、文档ID、内容、修改人、修改时间
- 公文流程聚合:流程ID、文档ID、当前环节、审批意见
这里有个设计要点:公文流程不要和审批流域共用同一个流程引擎。为什么?因为公文流程有严格的「会签」和「核稿」要求,和普通审批流的「一人审批通过就行」完全不同。强行复用会导致流程引擎越来越复杂,最后谁也改不动。
总结一下三个域的拆分对比:
| 业务域 | 核心实体 | 关键边界 | 常见误区 |
|---|---|---|---|
| 审批流 | 流程模板 | 模板 vs 实例 | 把审批记录混入流程实例 |
| 考勤 | 考勤周期 | 请假审批 vs 考勤结算 | 把请假单放在考勤域 |
| 公文 | 文档版本 | 公文流程 vs 普通审批 | 复用审批流引擎 |
3.5 拆分后的通信策略
域拆好了,怎么通信?我推荐用事件驱动的方式。
比如:
- 审批流域发布「审批通过」事件
- 考勤域订阅后,更新员工的请假天数
- 公文域订阅后,更新公文的签发状态
这样做的好处是:每个域只关心自己需要的数据,不需要知道其他域的内部细节。说白了,就是「你发布你的,我订阅我的,互不打扰」。
一个小技巧:
事件定义时,尽量用「过去时」命名。比如 LeaveApproved、DocumentSigned。这样语义更清晰,也符合事件溯源的思想。
最后说一句:DDD拆分不是一蹴而就的。我习惯先画一个「事件风暴图」,把业务域里的所有事件、命令、聚合都列出来,然后慢慢找边界。这个过程可能需要反复调整,但一旦边界清晰了,后面的微服务拆分就水到渠成了。