1. 战略设计到底要解决什么问题先说一个我观察到的现象。很多团队做系统架构,一上来就画微服务拆分图、设计数据库表、定义接口协议,忙活几个月,系统上线了,维护半年发现不对劲:业务一变更就要改一串服务,跨团队沟通全靠吼,一个需求拆成十几个任务还互相打架。问题出在哪?出在最开始就跳过了“战略设计”这一步。领域驱动设计(Domain-Driven Design,DDD)分为战略设计和战术设计两部分。战术设计讲的是聚合、实体、值对象、领域服务这些代码层面的实现手法,而战略设计是在写代码之前,先想清楚“这个业务领域到底长什么样”“系统要拆成哪几块”“每块之间的边界和关系是什么”。用大白话说,战略设计就是给系统画地图,战术设计才是按地图盖房子。很多人对DDD有个误解,以为它就是一套代码规范,学了一堆聚合和事件溯源就开始重构。实际上DDD最值钱的部分恰恰是战略设计,因为它解决的是“系统边界划分”和“团队协作模式”这两个最根本的问题。边界划错了,后面战术设计做得再漂亮也是白搭。这套方法论适合谁?适合那些正在做系统拆分、微服务改造、或者从单体架构向分布式架构演进的技术团队。也适合产品经理和业务人员一起参与——战略设计从来不是技术团队关起门来自己玩的事情,它需要业务专家和技术专家坐在一起,把对业务的理解对齐。我在实际项目里见过太多反面案例了。有一个订单系统,把用户信息、商品信息、订单信息全部揉在一个服务里,看起来是微服务架构,实际上还是单体,只是把部署拆开了。问题在于团队根本没有搞清楚业务上到底有哪些领域概念,边界是拍脑袋定的,结果就是服务之间耦合严重,改一个需求要动好几个服务。这种问题的根源,就是战略设计没做到位。2. 三个核心概念,缺一个都玩不转战略设计的核心概念有三个:统一语言、子域、限界上下文。这三个概念是层层递进的关系,理解透了,战略设计的框架就立起来了。2.1 统一语言:让业务方和技术方说同一种话先讲统一语言。做软件的人容易陷入一个误区,觉得技术术语更精确,跟业务沟通的时候满嘴“订单状态机”“库存SKU”“支付回调”,结果业务人员听得一头雾水。反过来,业务人员说“客户下单”“对账”“冲正”,技术人员又理解不到位。一个词在两边含义不一样,需求分析阶段就开始出现偏差。统一语言的本质,是业务专家和技术团队共同约定一套术语表,这套术语贯穿需求文档、代码注释、类命名、数据库字段、接口定义。比如“订单”这个词,业务上指的是用户提交的购买请求,那代码里就用Order,数据库表就叫订单表,接口就叫order,不要一会儿叫Trade一会儿叫Purchase。我做过一个电商项目,一开始团队里有人说“订单”,有人说“交易单”,还有人叫“流水单”,实际上指的是同一个业务概念,但因为叫法不统一,代码里出现了好几个意思相近的类,后续维护的人根本分不清哪个是哪个。后来我们花了半天时间,把核心业务概念梳理成一张术语表,所有人统一使用,沟通效率提升非常明显。术语表怎么建?我的经验是不要一上来就列一大堆,先列核心业务中最高频的20个左右的概念,够用就行。建术语表最好的时机就是事件风暴工作坊(后面会详细说),在梳理业务事件的过程中,发现同一个概念有不同叫法,当场就记下来,指定一个标准词。2.2 子域:识别业务的优先级和复杂度子域是业务领域的切分方式。DDD把整个业务领域分成三类:核心域、支撑域、通用域。核心域是业务最核心、最差异化、最值钱的部分,是企业竞争力的来源,需要投入最多的精力和资源去做好。比如电商平台的订单交易、库存计算,金融系统的风控和账务处理,这些都是核心域。支撑域是为核心域服务的必要模块,比如电商平台的商品管理、会员体系,它们在业务上不可或缺,但本身不构成核心竞争力。通用域是市面上已经成熟、用现成方案就能搞定的部分,比如权限管理、短信通知、文件存储,这类不需要自己花大力气去研发。为什么要区分这三类?因为资源永远是有限的,不可能每个模块都投入同样的精力。核心域要自己做、做精细,通用域可以买现成的产品或开源方案,支撑域可以适当简化。我见过不少团队在通用域上投入了大量人力做定制开发,核心域反而做得很粗糙,这就是子域识别出了问题。举一个具体的例子。一家做生鲜配送的公司,它的核心域是“配送到家的履约能力”——包括下单、拣货、配送路线规划、时效承诺。而“用户注册登录”是通用域,直接接一个成熟的认证服务就行。“商品上架”是支撑域,复杂度不高,用标准方案就能满足。如果团队把大部分精力花在改造自己的认证系统上,那就本末倒置了。识别子域最直接的方法,是看“这个模块如果做得不好,会不会直接影响公司的营收和竞争力”。会,就是核心域;不会但必须要有,就是支撑域;外面有大把成熟方案可替代,就是通用域。2.3 限界上下文:模型存在的边界限界上下文是战略设计里最重要的概念,也是很多人最不容易理解透的。一句话解释:一个模型只在某个明确的边界内才有效,出了这个边界,同一个词的含义可能完全不同。举个例子。“商品”这个词,在“商品管理”上下文里,指的是包含规格、库存、图片、描述的基础信息;在“搜索推荐”上下文里,指的是带有相关性权重、点击记录、标签化的索引对象;在“订单”上下文里,指的是被购买的那件物品,包含价格快照、优惠分摊等信息。同一个“商品”,在不同上下文里关心的是完全不同的侧面。如果你强行用一个统一的“商品模型”去满足所有场景,就会出现问题。搜索场景不想管库存和价格快照,订单场景不想被搜索的复杂标签干扰。最后这个模型会变得越来越臃肿,谁都用得不顺手。所以限界上下文的核心思想是:一个模型一个边界,边界内模型自洽,边界间通过明确的接口通信。这也直接对应了微服务拆分的原则——一个限界上下文通常就是一个微服务的边界。战略设计划定了限界上下文,微服务的粒度问题就自然解决了。我遇到过的最典型的错误,是把“订单”和“库存”放在一个服务里,理由是“下单要扣库存,放一起方便”。表面上看是简单了,实际上订单有订单的生命周期,库存有库存的流转逻辑,两者变化的节奏和原因完全不一样。一旦业务增速上来,订单服务和库存服务的扩容需求不同步,硬绑在一起就很别扭。正确的做法是分成两个限界上下文,订单上下文通过接口调用库存上下文的“扣减库存”服务,库存怎么管理,订单这边不用关心。3. 上下文映射:多个模型之间怎么相处限界上下文确定之后,接下来要处理的就是上下文之间的关系。上下文映射(Context Mapping)是描述这些关系的方式,也直接决定了服务之间采用什么集成模式。3.1 防腐层:保护自己不被别人带偏防腐层(Anti-Corruption Layer,ACL)是目前用得最多、也最推荐的一种模式。它的作用是:在集成外部上下文时,加一层转换逻辑,把外部模型翻译成自己上下文内部的模型,避免外部模型的变化直接影响内部实现。我举个非常生活化的例子。你要从国外买一台电器,这个电器的插头是欧标的,跟家里的插座不匹配。你不会把家里的墙拆了改成欧标插座,而是买一个转换插头,把欧标转成国标。防腐层就是这个转换插头。在系统层面,假设订单上下文需要从客户上下文中获取用户信息。客户上下文返回的“用户”包含几十个字段,但订单这边只需要用户ID和姓名。如果订单上下文直接使用客户上下文的对象,客户那边一改字段,订单这边就得跟着改。通过防腐层,订单上下文定义自己的“买家”模型,只保留需要的字段,在防腐层里完成映射转换,外部变化就被隔离了。防腐层的实现方式,通常是适配器模式。订单上下文定义一个买家仓储接口,内部实现了一个“客户服务适配器”,这个适配器负责调用客户上下文的接口,并把返回结果转换成订单上下文内部的买家模型。这样订单上下文的内部代码永远只跟自己的模型打交道,干净、清晰、好维护。3.2 开放主机服务和发布语言开放主机服务(Open Host Service,OHS)和发布语言(Published Language,PL)通常是搭配使用的。OHS指的是一个上下文对外提供一组明确的、稳定的服务接口,PL则是这组接口所使用的、对外发布的通用语言格式,通常用DTO(数据传输对象)来定义。举个例子,商品上下文对外发布一个商品查询服务,接口参数和返回值用的都是发布语言里定义的“商品DTO”。这个DTO是公开的、稳定的,其他上下文都按这个格式来对接。商品上下文内部怎么实现的,是聚合根还是领域服务,外部完全不用关心。为什么要这样做?核心目的是解耦。如果你把内部实体直接暴露给外部,内部一重构,所有人都受影响。通过OHS+PL,内部随便改,只要接口契约不变,外部就是安全的。在实际项目里,OHS+PL实际上就是我们现在常说的API设计规范。战略设计阶段就会把这些接口的契约定义清楚,让各个团队知道:我对外暴露什么服务,用什么数据格式,遵循什么语义。这比代码写完了再补接口文档要靠谱得多。3.3 其他几种上下文关系上下文映射还有其他几种关系模式,我把它们整理成一张表,方便对照。关系模式说明适用场景注意事项防腐层(ACL)增加转换层隔离外部模型集成老旧系统、外部第三方系统转换逻辑要注意性能和一致性问题开放主机服务(OHS)对外提供稳定的API接口核心上下文对外提供服务接口版本管理要提前规划发布语言(PL)统一定义接口数据格式与OHS搭配使用DTO字段变更要做兼容方案共享内核(Shared Kernel)多个上下文共享一小部分模型关系紧密的小团队合作共享部分要精挑细选,容易失控客户-供应商(C/S)下游依赖上游,上游主导节奏团队间有明确依赖关系上游变接口要提前通知下游各自为政(Separate Ways)完全独立,没有集成集成成本高于收益时数据不一致要能接受共享内核是一个要慎用的模式。两个上下文共享一段代码或者一套模型,看起来省事,但实际上要求两个团队的开发节奏高度同步,否则一个人改了共享代码,另一个就被破坏了。除非两个团队的人员是高度重合的、物理距离很近、沟通成本极低,否则我不推荐使用共享内核。客户-供应商模式在企业内部很常见。比如支付上下文是上游,订单上下文是下游,支付接口一变,订单那边就得跟着改。这种情况下要特别重视接口版本的变更管理,下游要提前收到通知,最好有适配层缓冲,避免被上游的变更直接打到脸上。4. 实操:从业务梳理到战略设计落地概念说了一堆,落到实际操作上怎么干?我以自己参与过的一个库存管理系统的战略设计为例,完整走一遍流程。这个方法就是业界常用的事件风暴(Event Storming)。4.1 第一步:组织事件风暴工作坊事件风暴是一种工作坊式的建模方法,核心方式是让业务人员和技术人员坐在一起,在墙上张贴便利贴,共同梳理业务流程。参与者需要两类人:一类是熟悉业务的业务专家,一类是懂技术架构的研发骨干。两类人都必须全程在场,缺了任何一方,建模质量都会大打折扣。工作坊的第一步,是让业务专家按照时间顺序,把业务过程里发生的领域事件全部贴出来。领域事件是过去时态的短句,比如“订单已创建”“库存已锁定”“商品已发货”“退款已发起”。一张便利贴写一个事件,按时间线从前往后排开。这里有个小技巧,可以用不同颜色的便利贴区分不同类别的信息,比如橙色贴事件、蓝色贴命令、绿色贴聚合,这样墙上的信息层次会很清晰。做这件事的时候,你会发现业务专家和技术专家之间对同一个事件的理解会很不一样。业务专家觉得“库存已锁定”和“库存已扣减”是一回事,技术人员觉得这是两个完全不同的状态,代表不同的处理阶段。这些讨论本身就是价值所在——统一语言就是在这些碰撞中逐步建立的。4.2 第二步:识别限界上下文事件都贴出来之后,开始寻找边界。方法很简单:看哪些事件是围绕同一个业务概念在变化的,变化原因和节奏一致,就归为一个候选上下文。比如“库存已新增”“库存已锁定”“库存已扣减”“库存已盘盈”,这些事件的核心主语都是“库存”,而且它们的生命周期是连贯的,那就归入“库存上下文”。判断边界有个很实用的辅助技巧:看“矛盾点”。如果两个业务概念之间存在明显冲突,比如订单上要的是“可用库存”,仓库管理要的是“物理库存”,这两个概念在业务上是同时存在且含义不同的,那它们很可能应该分属不同的限界上下文。实战中,限界上下文的颗粒度跟团队组织结构和系统目标密切相关。一般来说,一个中小型系统拆分出5~10个限界上下文是比较常见的。拆得太细会导致集成成本过高,拆得太粗又会回到大泥球的老路,关键还是根据业务复杂度来判断。4.3 第三步:画出上下文映射图上下文划分完成之后,下一步就是在每个上下文之间连线,标注它们的关系模式。库存上下文需要从商品上下文获取商品基础信息,那就画一条“库存上下文→商品上下文”的箭头,标注客户-供应商关系。订单上下文需要调用库存上下文的扣减库存服务,就标注防腐层关系。画完上下文映射图,有两件事要特别关注。第一,所有箭头指向的方向上,上下游关系要清晰。如果A依赖B、B又依赖A,出现循环依赖,那就是边界划分有问题,要重新审视。第二,团队划分要跟上下文边界尽量保持一致。如果一个团队负责两个限界上下文,而且这两个上下文之间交互很频繁,那要考虑是不是应该合并;反之,一个上下文拆成两个团队来维护,也要小心沟通成本飙升。上下文映射图是战略设计最重要的产出物之一。它是整个系统拆分方案的蓝图,也是后续微服务拆分的直接依据。一份好的上下文映射图,应当清晰反映出系统的模块结构、集成方式、依赖方向。4.4 第四步:定义核心域与优先级在上下文映射图基础上,再把每一个限界上下文标记为“核心域”“支撑域”或“通用域”。这个标记对资源投入有直接指导意义。库存管理系统里,“库存计算”和“库存锁定”这些直接关系到订单能否履约的部分,毫无疑问是核心域,要投入最好的工程师、花最多的精力去设计。“商品信息维护”属于支撑域,用常规方案做就行。“权限管理”“操作日志”属于通用域,直接接成熟框架甚至可以外包。这样划分的作用在于,团队开会讨论架构方案时,能把讨论的焦点先集中到核心域上。核心域值得花大量的时间反复验证设计,支撑域和通用域则能省则省,用最朴素的实现方式。我的经验是,凡是在支撑域和通用域上无限纠结方案的团队,多半是没有真正理解核心域在哪里。4.5 战略设计的最终产出物战略设计做完了,团队应该拿到哪些东西才算合格?第一份是统一语言术语表。核心业务概念有标准定义,团队所有人都遵循同一套术语。第二份是限界上下文清单。每个上下文有清晰的职责描述,边界明确,模型自洽。第三份是上下文映射图。上下文之间的依赖关系和集成模式一目了然。第四份是核心域标识。明确哪个上下文是核心域,哪个是支撑域,哪个是通用域。这四份产出物,不需要写厚厚的文档,一张大白板、几个表格就能承载。关键是内容要经过业务和技术双方共同确认,而不是技术团队自己闭门造车得出的结论。5. 实战中踩过的坑与排查思路战略设计这套方法论看着不复杂,落地的时候坑不少。我把自己实战中踩过的坑和积累的经验整理一下。5.1 常见问题速查表问题表现可能原因处理建议限界上下文拆了又拆,服务越来越多把业务模型和物理部署粒度混为一谈先按业务边界划分,再根据部署和团队情况决定合并策略上下文映射图画得明明白白,代码实现却耦合战术设计没有遵守边界,跨上下文直接调用内部方法通过架构守护测试或代码扫描工具强制校验依赖方向业务方和技术方对同一概念的理解始终不一致统一语言没有真正落地,只在文档里存在在迭代会议上持续使用术语表,代码评审校验命名统一核心域没有突出出来,所有模块投入一样子域识别不够认真重新和业务方确认“什么最能赚钱”,按这个标准划分上下文间存在循环依赖边界划分出现问题重新审视职责划分,必要时引入事件机制切断循环表格里最后这个循环依赖要多说两句。A上下文需要B的数据,B上下文也需要A的数据,画图上就会出现双向箭头。这种情况说明两个上下文边界没切干净,职责有重叠。我处理过的一个办法是:把循环依赖中重叠的部分向下提取成一个独立的新上下文,让A和B都只依赖新上下文,循环就被切断了。5.2 关于事件风暴的几个心得事件风暴看起来简单,实际操作中要注意控制节奏。我发现最常见的问题是参与者太多、时间太长,最后变成低效的头脑风暴。我的建议是:一场事件风暴控制在3~4小时以内,参与者控制在5~8人,业务专家和技术骨干必须全程在场,其他人可以选择性参与。人太多,讨论会发散;人太少,信息覆盖不全面。另一个心得是,事件风暴不必追求一次性完成。第一轮梳理出主干流程,第二轮补充异常分支和边界情况,必要时再来一轮细化。我见过有团队开了一整天的事件风暴,到下午大家已经疲惫不堪,后半段的建模质量明显下降。分几次进行,效果会好很多。还有一点很重要:事件风暴是开始而不是结束。热热闹闹开完工作坊,墙上的便利贴拍完照、画成图,最后落进文档里吃灰,这没有任何用。真正有价值的是后续在开发过程中持续维护这些边界,每次迭代开计划会的时候,都对照上下文映射图检查一下当前的需求变更落在哪个上下文里。5.3 战略设计不是一次性的活动系统是会演进的,业务会变,组织的分工也会变。战略设计做的是一张当前时刻的地图,不代表未来永远不变。我见过不少团队把战略设计当成项目启动时的仪式,做完就再也不看一眼,一年之后系统结构已经跟当初的设计面目全非。我的习惯是:每隔一定周期,结合系统现状重新审视一次限界上下文划分。看新增的业务模块是不是塞进了不合适的上下文,看上下文间的依赖是不是出现了意料之外的绕过路径,看核心域是不是还是当初划的那个核心域。架构治理是持续的事情,战略设计的产物需要像代码一样被维护、被评审。从方法本身来说,战略设计谈不上高深,它更多的是一种思维方式的转变:先想清楚业务的本质,再决定技术方案。很多人之所以觉得DDD落地难,其实一开始就没把战略设计这一步做扎实。我在实际项目中最大的体会是:当团队把精力投入在建立统一语言、划清限界上下文、认清核心域这些看似“不写代码”的事情上时,后面写代码的速度反而会变快,因为方向对了,所有的力气都不会白费。
1. 战略设计到底要解决什么问题先说一个我观察到的现象。很多团队做系统架构,一上来就画微服务拆分图、设计数据库表、定义接口协议,忙活几个月,系统上线了,维护半年发现不对劲:业务一变更就要改一串服务,跨团队沟通全靠吼,一个需求拆成十几个任务还互相打架。问题出在哪?出在最开始就跳过了“战略设计”这一步。领域驱动设计(Domain-Driven Design,DDD)分为战略设计和战术设计两部分。战术设计讲的是聚合、实体、值对象、领域服务这些代码层面的实现手法,而战略设计是在写代码之前,先想清楚“这个业务领域到底长什么样”“系统要拆成哪几块”“每块之间的边界和关系是什么”。用大白话说,战略设计就是给系统画地图,战术设计才是按地图盖房子。很多人对DDD有个误解,以为它就是一套代码规范,学了一堆聚合和事件溯源就开始重构。实际上DDD最值钱的部分恰恰是战略设计,因为它解决的是“系统边界划分”和“团队协作模式”这两个最根本的问题。边界划错了,后面战术设计做得再漂亮也是白搭。这套方法论适合谁?适合那些正在做系统拆分、微服务改造、或者从单体架构向分布式架构演进的技术团队。也适合产品经理和业务人员一起参与——战略设计从来不是技术团队关起门来自己玩的事情,它需要业务专家和技术专家坐在一起,把对业务的理解对齐。我在实际项目里见过太多反面案例了。有一个订单系统,把用户信息、商品信息、订单信息全部揉在一个服务里,看起来是微服务架构,实际上还是单体,只是把部署拆开了。问题在于团队根本没有搞清楚业务上到底有哪些领域概念,边界是拍脑袋定的,结果就是服务之间耦合严重,改一个需求要动好几个服务。这种问题的根源,就是战略设计没做到位。2. 三个核心概念,缺一个都玩不转战略设计的核心概念有三个:统一语言、子域、限界上下文。这三个概念是层层递进的关系,理解透了,战略设计的框架就立起来了。2.1 统一语言:让业务方和技术方说同一种话先讲统一语言。做软件的人容易陷入一个误区,觉得技术术语更精确,跟业务沟通的时候满嘴“订单状态机”“库存SKU”“支付回调”,结果业务人员听得一头雾水。反过来,业务人员说“客户下单”“对账”“冲正”,技术人员又理解不到位。一个词在两边含义不一样,需求分析阶段就开始出现偏差。统一语言的本质,是业务专家和技术团队共同约定一套术语表,这套术语贯穿需求文档、代码注释、类命名、数据库字段、接口定义。比如“订单”这个词,业务上指的是用户提交的购买请求,那代码里就用Order,数据库表就叫订单表,接口就叫order,不要一会儿叫Trade一会儿叫Purchase。我做过一个电商项目,一开始团队里有人说“订单”,有人说“交易单”,还有人叫“流水单”,实际上指的是同一个业务概念,但因为叫法不统一,代码里出现了好几个意思相近的类,后续维护的人根本分不清哪个是哪个。后来我们花了半天时间,把核心业务概念梳理成一张术语表,所有人统一使用,沟通效率提升非常明显。术语表怎么建?我的经验是不要一上来就列一大堆,先列核心业务中最高频的20个左右的概念,够用就行。建术语表最好的时机就是事件风暴工作坊(后面会详细说),在梳理业务事件的过程中,发现同一个概念有不同叫法,当场就记下来,指定一个标准词。2.2 子域:识别业务的优先级和复杂度子域是业务领域的切分方式。DDD把整个业务领域分成三类:核心域、支撑域、通用域。核心域是业务最核心、最差异化、最值钱的部分,是企业竞争力的来源,需要投入最多的精力和资源去做好。比如电商平台的订单交易、库存计算,金融系统的风控和账务处理,这些都是核心域。支撑域是为核心域服务的必要模块,比如电商平台的商品管理、会员体系,它们在业务上不可或缺,但本身不构成核心竞争力。通用域是市面上已经成熟、用现成方案就能搞定的部分,比如权限管理、短信通知、文件存储,这类不需要自己花大力气去研发。为什么要区分这三类?因为资源永远是有限的,不可能每个模块都投入同样的精力。核心域要自己做、做精细,通用域可以买现成的产品或开源方案,支撑域可以适当简化。我见过不少团队在通用域上投入了大量人力做定制开发,核心域反而做得很粗糙,这就是子域识别出了问题。举一个具体的例子。一家做生鲜配送的公司,它的核心域是“配送到家的履约能力”——包括下单、拣货、配送路线规划、时效承诺。而“用户注册登录”是通用域,直接接一个成熟的认证服务就行。“商品上架”是支撑域,复杂度不高,用标准方案就能满足。如果团队把大部分精力花在改造自己的认证系统上,那就本末倒置了。识别子域最直接的方法,是看“这个模块如果做得不好,会不会直接影响公司的营收和竞争力”。会,就是核心域;不会但必须要有,就是支撑域;外面有大把成熟方案可替代,就是通用域。2.3 限界上下文:模型存在的边界限界上下文是战略设计里最重要的概念,也是很多人最不容易理解透的。一句话解释:一个模型只在某个明确的边界内才有效,出了这个边界,同一个词的含义可能完全不同。举个例子。“商品”这个词,在“商品管理”上下文里,指的是包含规格、库存、图片、描述的基础信息;在“搜索推荐”上下文里,指的是带有相关性权重、点击记录、标签化的索引对象;在“订单”上下文里,指的是被购买的那件物品,包含价格快照、优惠分摊等信息。同一个“商品”,在不同上下文里关心的是完全不同的侧面。如果你强行用一个统一的“商品模型”去满足所有场景,就会出现问题。搜索场景不想管库存和价格快照,订单场景不想被搜索的复杂标签干扰。最后这个模型会变得越来越臃肿,谁都用得不顺手。所以限界上下文的核心思想是:一个模型一个边界,边界内模型自洽,边界间通过明确的接口通信。这也直接对应了微服务拆分的原则——一个限界上下文通常就是一个微服务的边界。战略设计划定了限界上下文,微服务的粒度问题就自然解决了。我遇到过的最典型的错误,是把“订单”和“库存”放在一个服务里,理由是“下单要扣库存,放一起方便”。表面上看是简单了,实际上订单有订单的生命周期,库存有库存的流转逻辑,两者变化的节奏和原因完全不一样。一旦业务增速上来,订单服务和库存服务的扩容需求不同步,硬绑在一起就很别扭。正确的做法是分成两个限界上下文,订单上下文通过接口调用库存上下文的“扣减库存”服务,库存怎么管理,订单这边不用关心。3. 上下文映射:多个模型之间怎么相处限界上下文确定之后,接下来要处理的就是上下文之间的关系。上下文映射(Context Mapping)是描述这些关系的方式,也直接决定了服务之间采用什么集成模式。3.1 防腐层:保护自己不被别人带偏防腐层(Anti-Corruption Layer,ACL)是目前用得最多、也最推荐的一种模式。它的作用是:在集成外部上下文时,加一层转换逻辑,把外部模型翻译成自己上下文内部的模型,避免外部模型的变化直接影响内部实现。我举个非常生活化的例子。你要从国外买一台电器,这个电器的插头是欧标的,跟家里的插座不匹配。你不会把家里的墙拆了改成欧标插座,而是买一个转换插头,把欧标转成国标。防腐层就是这个转换插头。在系统层面,假设订单上下文需要从客户上下文中获取用户信息。客户上下文返回的“用户”包含几十个字段,但订单这边只需要用户ID和姓名。如果订单上下文直接使用客户上下文的对象,客户那边一改字段,订单这边就得跟着改。通过防腐层,订单上下文定义自己的“买家”模型,只保留需要的字段,在防腐层里完成映射转换,外部变化就被隔离了。防腐层的实现方式,通常是适配器模式。订单上下文定义一个买家仓储接口,内部实现了一个“客户服务适配器”,这个适配器负责调用客户上下文的接口,并把返回结果转换成订单上下文内部的买家模型。这样订单上下文的内部代码永远只跟自己的模型打交道,干净、清晰、好维护。3.2 开放主机服务和发布语言开放主机服务(Open Host Service,OHS)和发布语言(Published Language,PL)通常是搭配使用的。OHS指的是一个上下文对外提供一组明确的、稳定的服务接口,PL则是这组接口所使用的、对外发布的通用语言格式,通常用DTO(数据传输对象)来定义。举个例子,商品上下文对外发布一个商品查询服务,接口参数和返回值用的都是发布语言里定义的“商品DTO”。这个DTO是公开的、稳定的,其他上下文都按这个格式来对接。商品上下文内部怎么实现的,是聚合根还是领域服务,外部完全不用关心。为什么要这样做?核心目的是解耦。如果你把内部实体直接暴露给外部,内部一重构,所有人都受影响。通过OHS+PL,内部随便改,只要接口契约不变,外部就是安全的。在实际项目里,OHS+PL实际上就是我们现在常说的API设计规范。战略设计阶段就会把这些接口的契约定义清楚,让各个团队知道:我对外暴露什么服务,用什么数据格式,遵循什么语义。这比代码写完了再补接口文档要靠谱得多。3.3 其他几种上下文关系上下文映射还有其他几种关系模式,我把它们整理成一张表,方便对照。关系模式说明适用场景注意事项防腐层(ACL)增加转换层隔离外部模型集成老旧系统、外部第三方系统转换逻辑要注意性能和一致性问题开放主机服务(OHS)对外提供稳定的API接口核心上下文对外提供服务接口版本管理要提前规划发布语言(PL)统一定义接口数据格式与OHS搭配使用DTO字段变更要做兼容方案共享内核(Shared Kernel)多个上下文共享一小部分模型关系紧密的小团队合作共享部分要精挑细选,容易失控客户-供应商(C/S)下游依赖上游,上游主导节奏团队间有明确依赖关系上游变接口要提前通知下游各自为政(Separate Ways)完全独立,没有集成集成成本高于收益时数据不一致要能接受共享内核是一个要慎用的模式。两个上下文共享一段代码或者一套模型,看起来省事,但实际上要求两个团队的开发节奏高度同步,否则一个人改了共享代码,另一个就被破坏了。除非两个团队的人员是高度重合的、物理距离很近、沟通成本极低,否则我不推荐使用共享内核。客户-供应商模式在企业内部很常见。比如支付上下文是上游,订单上下文是下游,支付接口一变,订单那边就得跟着改。这种情况下要特别重视接口版本的变更管理,下游要提前收到通知,最好有适配层缓冲,避免被上游的变更直接打到脸上。4. 实操:从业务梳理到战略设计落地概念说了一堆,落到实际操作上怎么干?我以自己参与过的一个库存管理系统的战略设计为例,完整走一遍流程。这个方法就是业界常用的事件风暴(Event Storming)。4.1 第一步:组织事件风暴工作坊事件风暴是一种工作坊式的建模方法,核心方式是让业务人员和技术人员坐在一起,在墙上张贴便利贴,共同梳理业务流程。参与者需要两类人:一类是熟悉业务的业务专家,一类是懂技术架构的研发骨干。两类人都必须全程在场,缺了任何一方,建模质量都会大打折扣。工作坊的第一步,是让业务专家按照时间顺序,把业务过程里发生的领域事件全部贴出来。领域事件是过去时态的短句,比如“订单已创建”“库存已锁定”“商品已发货”“退款已发起”。一张便利贴写一个事件,按时间线从前往后排开。这里有个小技巧,可以用不同颜色的便利贴区分不同类别的信息,比如橙色贴事件、蓝色贴命令、绿色贴聚合,这样墙上的信息层次会很清晰。做这件事的时候,你会发现业务专家和技术专家之间对同一个事件的理解会很不一样。业务专家觉得“库存已锁定”和“库存已扣减”是一回事,技术人员觉得这是两个完全不同的状态,代表不同的处理阶段。这些讨论本身就是价值所在——统一语言就是在这些碰撞中逐步建立的。4.2 第二步:识别限界上下文事件都贴出来之后,开始寻找边界。方法很简单:看哪些事件是围绕同一个业务概念在变化的,变化原因和节奏一致,就归为一个候选上下文。比如“库存已新增”“库存已锁定”“库存已扣减”“库存已盘盈”,这些事件的核心主语都是“库存”,而且它们的生命周期是连贯的,那就归入“库存上下文”。判断边界有个很实用的辅助技巧:看“矛盾点”。如果两个业务概念之间存在明显冲突,比如订单上要的是“可用库存”,仓库管理要的是“物理库存”,这两个概念在业务上是同时存在且含义不同的,那它们很可能应该分属不同的限界上下文。实战中,限界上下文的颗粒度跟团队组织结构和系统目标密切相关。一般来说,一个中小型系统拆分出5~10个限界上下文是比较常见的。拆得太细会导致集成成本过高,拆得太粗又会回到大泥球的老路,关键还是根据业务复杂度来判断。4.3 第三步:画出上下文映射图上下文划分完成之后,下一步就是在每个上下文之间连线,标注它们的关系模式。库存上下文需要从商品上下文获取商品基础信息,那就画一条“库存上下文→商品上下文”的箭头,标注客户-供应商关系。订单上下文需要调用库存上下文的扣减库存服务,就标注防腐层关系。画完上下文映射图,有两件事要特别关注。第一,所有箭头指向的方向上,上下游关系要清晰。如果A依赖B、B又依赖A,出现循环依赖,那就是边界划分有问题,要重新审视。第二,团队划分要跟上下文边界尽量保持一致。如果一个团队负责两个限界上下文,而且这两个上下文之间交互很频繁,那要考虑是不是应该合并;反之,一个上下文拆成两个团队来维护,也要小心沟通成本飙升。上下文映射图是战略设计最重要的产出物之一。它是整个系统拆分方案的蓝图,也是后续微服务拆分的直接依据。一份好的上下文映射图,应当清晰反映出系统的模块结构、集成方式、依赖方向。4.4 第四步:定义核心域与优先级在上下文映射图基础上,再把每一个限界上下文标记为“核心域”“支撑域”或“通用域”。这个标记对资源投入有直接指导意义。库存管理系统里,“库存计算”和“库存锁定”这些直接关系到订单能否履约的部分,毫无疑问是核心域,要投入最好的工程师、花最多的精力去设计。“商品信息维护”属于支撑域,用常规方案做就行。“权限管理”“操作日志”属于通用域,直接接成熟框架甚至可以外包。这样划分的作用在于,团队开会讨论架构方案时,能把讨论的焦点先集中到核心域上。核心域值得花大量的时间反复验证设计,支撑域和通用域则能省则省,用最朴素的实现方式。我的经验是,凡是在支撑域和通用域上无限纠结方案的团队,多半是没有真正理解核心域在哪里。4.5 战略设计的最终产出物战略设计做完了,团队应该拿到哪些东西才算合格?第一份是统一语言术语表。核心业务概念有标准定义,团队所有人都遵循同一套术语。第二份是限界上下文清单。每个上下文有清晰的职责描述,边界明确,模型自洽。第三份是上下文映射图。上下文之间的依赖关系和集成模式一目了然。第四份是核心域标识。明确哪个上下文是核心域,哪个是支撑域,哪个是通用域。这四份产出物,不需要写厚厚的文档,一张大白板、几个表格就能承载。关键是内容要经过业务和技术双方共同确认,而不是技术团队自己闭门造车得出的结论。5. 实战中踩过的坑与排查思路战略设计这套方法论看着不复杂,落地的时候坑不少。我把自己实战中踩过的坑和积累的经验整理一下。5.1 常见问题速查表问题表现可能原因处理建议限界上下文拆了又拆,服务越来越多把业务模型和物理部署粒度混为一谈先按业务边界划分,再根据部署和团队情况决定合并策略上下文映射图画得明明白白,代码实现却耦合战术设计没有遵守边界,跨上下文直接调用内部方法通过架构守护测试或代码扫描工具强制校验依赖方向业务方和技术方对同一概念的理解始终不一致统一语言没有真正落地,只在文档里存在在迭代会议上持续使用术语表,代码评审校验命名统一核心域没有突出出来,所有模块投入一样子域识别不够认真重新和业务方确认“什么最能赚钱”,按这个标准划分上下文间存在循环依赖边界划分出现问题重新审视职责划分,必要时引入事件机制切断循环表格里最后这个循环依赖要多说两句。A上下文需要B的数据,B上下文也需要A的数据,画图上就会出现双向箭头。这种情况说明两个上下文边界没切干净,职责有重叠。我处理过的一个办法是:把循环依赖中重叠的部分向下提取成一个独立的新上下文,让A和B都只依赖新上下文,循环就被切断了。5.2 关于事件风暴的几个心得事件风暴看起来简单,实际操作中要注意控制节奏。我发现最常见的问题是参与者太多、时间太长,最后变成低效的头脑风暴。我的建议是:一场事件风暴控制在3~4小时以内,参与者控制在5~8人,业务专家和技术骨干必须全程在场,其他人可以选择性参与。人太多,讨论会发散;人太少,信息覆盖不全面。另一个心得是,事件风暴不必追求一次性完成。第一轮梳理出主干流程,第二轮补充异常分支和边界情况,必要时再来一轮细化。我见过有团队开了一整天的事件风暴,到下午大家已经疲惫不堪,后半段的建模质量明显下降。分几次进行,效果会好很多。还有一点很重要:事件风暴是开始而不是结束。热热闹闹开完工作坊,墙上的便利贴拍完照、画成图,最后落进文档里吃灰,这没有任何用。真正有价值的是后续在开发过程中持续维护这些边界,每次迭代开计划会的时候,都对照上下文映射图检查一下当前的需求变更落在哪个上下文里。5.3 战略设计不是一次性的活动系统是会演进的,业务会变,组织的分工也会变。战略设计做的是一张当前时刻的地图,不代表未来永远不变。我见过不少团队把战略设计当成项目启动时的仪式,做完就再也不看一眼,一年之后系统结构已经跟当初的设计面目全非。我的习惯是:每隔一定周期,结合系统现状重新审视一次限界上下文划分。看新增的业务模块是不是塞进了不合适的上下文,看上下文间的依赖是不是出现了意料之外的绕过路径,看核心域是不是还是当初划的那个核心域。架构治理是持续的事情,战略设计的产物需要像代码一样被维护、被评审。从方法本身来说,战略设计谈不上高深,它更多的是一种思维方式的转变:先想清楚业务的本质,再决定技术方案。很多人之所以觉得DDD落地难,其实一开始就没把战略设计这一步做扎实。我在实际项目中最大的体会是:当团队把精力投入在建立统一语言、划清限界上下文、认清核心域这些看似“不写代码”的事情上时,后面写代码的速度反而会变快,因为方向对了,所有的力气都不会白费。