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 拆分后的通信策略

域拆好了,怎么通信?我推荐用事件驱动的方式。

比如:

  • 审批流域发布「审批通过」事件
  • 考勤域订阅后,更新员工的请假天数
  • 公文域订阅后,更新公文的签发状态

这样做的好处是:每个域只关心自己需要的数据,不需要知道其他域的内部细节。说白了,就是「你发布你的,我订阅我的,互不打扰」。

一个小技巧:

事件定义时,尽量用「过去时」命名。比如 LeaveApprovedDocumentSigned。这样语义更清晰,也符合事件溯源的思想。

最后说一句:DDD拆分不是一蹴而就的。我习惯先画一个「事件风暴图」,把业务域里的所有事件、命令、聚合都列出来,然后慢慢找边界。这个过程可能需要反复调整,但一旦边界清晰了,后面的微服务拆分就水到渠成了。