1. 战略设计到底要解决什么问题先说一个我观察到的现象。很多团队做系统架构一上来就画微服务拆分图、设计数据库表、定义接口协议忙活几个月系统上线了维护半年发现不对劲业务一变更就要改一串服务跨团队沟通全靠吼一个需求拆成十几个任务还互相打架。问题出在哪出在最开始就跳过了“战略设计”这一步。领域驱动设计Domain-Driven DesignDDD分为战略设计和战术设计两部分。战术设计讲的是聚合、实体、值对象、领域服务这些代码层面的实现手法而战略设计是在写代码之前先想清楚“这个业务领域到底长什么样”“系统要拆成哪几块”“每块之间的边界和关系是什么”。用大白话说战略设计就是给系统画地图战术设计才是按地图盖房子。很多人对DDD有个误解以为它就是一套代码规范学了一堆聚合和事件溯源就开始重构。实际上DDD最值钱的部分恰恰是战略设计因为它解决的是“系统边界划分”和“团队协作模式”这两个最根本的问题。边界划错了后面战术设计做得再漂亮也是白搭。这套方法论适合谁适合那些正在做系统拆分、微服务改造、或者从单体架构向分布式架构演进的技术团队。也适合产品经理和业务人员一起参与——战略设计从来不是技术团队关起门来自己玩的事情它需要业务专家和技术专家坐在一起把对业务的理解对齐。我在实际项目里见过太多反面案例了。有一个订单系统把用户信息、商品信息、订单信息全部揉在一个服务里看起来是微服务架构实际上还是单体只是把部署拆开了。问题在于团队根本没有搞清楚业务上到底有哪些领域概念边界是拍脑袋定的结果就是服务之间耦合严重改一个需求要动好几个服务。这种问题的根源就是战略设计没做到位。2. 三个核心概念缺一个都玩不转战略设计的核心概念有三个统一语言、子域、限界上下文。这三个概念是层层递进的关系理解透了战略设计的框架就立起来了。2.1 统一语言让业务方和技术方说同一种话先讲统一语言。做软件的人容易陷入一个误区觉得技术术语更精确跟业务沟通的时候满嘴“订单状态机”“库存SKU”“支付回调”结果业务人员听得一头雾水。反过来业务人员说“客户下单”“对账”“冲正”技术人员又理解不到位。一个词在两边含义不一样需求分析阶段就开始出现偏差。统一语言的本质是业务专家和技术团队共同约定一套术语表这套术语贯穿需求文档、代码注释、类命名、数据库字段、接口定义。比如“订单”这个词业务上指的是用户提交的购买请求那代码里就用Order数据库表就叫订单表接口就叫order不要一会儿叫Trade一会儿叫Purchase。我做过一个电商项目一开始团队里有人说“订单”有人说“交易单”还有人叫“流水单”实际上指的是同一个业务概念但因为叫法不统一代码里出现了好几个意思相近的类后续维护的人根本分不清哪个是哪个。后来我们花了半天时间把核心业务概念梳理成一张术语表所有人统一使用沟通效率提升非常明显。术语表怎么建我的经验是不要一上来就列一大堆先列核心业务中最高频的20个左右的概念够用就行。建术语表最好的时机就是事件风暴工作坊后面会详细说在梳理业务事件的过程中发现同一个概念有不同叫法当场就记下来指定一个标准词。2.2 子域识别业务的优先级和复杂度子域是业务领域的切分方式。DDD把整个业务领域分成三类核心域、支撑域、通用域。核心域是业务最核心、最差异化、最值钱的部分是企业竞争力的来源需要投入最多的精力和资源去做好。比如电商平台的订单交易、库存计算金融系统的风控和账务处理这些都是核心域。支撑域是为核心域服务的必要模块比如电商平台的商品管理、会员体系它们在业务上不可或缺但本身不构成核心竞争力。通用域是市面上已经成熟、用现成方案就能搞定的部分比如权限管理、短信通知、文件存储这类不需要自己花大力气去研发。为什么要区分这三类因为资源永远是有限的不可能每个模块都投入同样的精力。核心域要自己做、做精细通用域可以买现成的产品或开源方案支撑域可以适当简化。我见过不少团队在通用域上投入了大量人力做定制开发核心域反而做得很粗糙这就是子域识别出了问题。举一个具体的例子。一家做生鲜配送的公司它的核心域是“配送到家的履约能力”——包括下单、拣货、配送路线规划、时效承诺。而“用户注册登录”是通用域直接接一个成熟的认证服务就行。“商品上架”是支撑域复杂度不高用标准方案就能满足。如果团队把大部分精力花在改造自己的认证系统上那就本末倒置了。识别子域最直接的方法是看“这个模块如果做得不好会不会直接影响公司的营收和竞争力”。会就是核心域不会但必须要有就是支撑域外面有大把成熟方案可替代就是通用域。2.3 限界上下文模型存在的边界限界上下文是战略设计里最重要的概念也是很多人最不容易理解透的。一句话解释一个模型只在某个明确的边界内才有效出了这个边界同一个词的含义可能完全不同。举个例子。“商品”这个词在“商品管理”上下文里指的是包含规格、库存、图片、描述的基础信息在“搜索推荐”上下文里指的是带有相关性权重、点击记录、标签化的索引对象在“订单”上下文里指的是被购买的那件物品包含价格快照、优惠分摊等信息。同一个“商品”在不同上下文里关心的是完全不同的侧面。如果你强行用一个统一的“商品模型”去满足所有场景就会出现问题。搜索场景不想管库存和价格快照订单场景不想被搜索的复杂标签干扰。最后这个模型会变得越来越臃肿谁都用得不顺手。所以限界上下文的核心思想是一个模型一个边界边界内模型自洽边界间通过明确的接口通信。这也直接对应了微服务拆分的原则——一个限界上下文通常就是一个微服务的边界。战略设计划定了限界上下文微服务的粒度问题就自然解决了。我遇到过的最典型的错误是把“订单”和“库存”放在一个服务里理由是“下单要扣库存放一起方便”。表面上看是简单了实际上订单有订单的生命周期库存有库存的流转逻辑两者变化的节奏和原因完全不一样。一旦业务增速上来订单服务和库存服务的扩容需求不同步硬绑在一起就很别扭。正确的做法是分成两个限界上下文订单上下文通过接口调用库存上下文的“扣减库存”服务库存怎么管理订单这边不用关心。3. 上下文映射多个模型之间怎么相处限界上下文确定之后接下来要处理的就是上下文之间的关系。上下文映射Context Mapping是描述这些关系的方式也直接决定了服务之间采用什么集成模式。3.1 防腐层保护自己不被别人带偏防腐层Anti-Corruption LayerACL是目前用得最多、也最推荐的一种模式。它的作用是在集成外部上下文时加一层转换逻辑把外部模型翻译成自己上下文内部的模型避免外部模型的变化直接影响内部实现。我举个非常生活化的例子。你要从国外买一台电器这个电器的插头是欧标的跟家里的插座不匹配。你不会把家里的墙拆了改成欧标插座而是买一个转换插头把欧标转成国标。防腐层就是这个转换插头。在系统层面假设订单上下文需要从客户上下文中获取用户信息。客户上下文返回的“用户”包含几十个字段但订单这边只需要用户ID和姓名。如果订单上下文直接使用客户上下文的对象客户那边一改字段订单这边就得跟着改。通过防腐层订单上下文定义自己的“买家”模型只保留需要的字段在防腐层里完成映射转换外部变化就被隔离了。防腐层的实现方式通常是适配器模式。订单上下文定义一个买家仓储接口内部实现了一个“客户服务适配器”这个适配器负责调用客户上下文的接口并把返回结果转换成订单上下文内部的买家模型。这样订单上下文的内部代码永远只跟自己的模型打交道干净、清晰、好维护。3.2 开放主机服务和发布语言开放主机服务Open Host ServiceOHS和发布语言Published LanguagePL通常是搭配使用的。OHS指的是一个上下文对外提供一组明确的、稳定的服务接口PL则是这组接口所使用的、对外发布的通用语言格式通常用DTO数据传输对象来定义。举个例子商品上下文对外发布一个商品查询服务接口参数和返回值用的都是发布语言里定义的“商品DTO”。这个DTO是公开的、稳定的其他上下文都按这个格式来对接。商品上下文内部怎么实现的是聚合根还是领域服务外部完全不用关心。为什么要这样做核心目的是解耦。如果你把内部实体直接暴露给外部内部一重构所有人都受影响。通过OHSPL内部随便改只要接口契约不变外部就是安全的。在实际项目里OHSPL实际上就是我们现在常说的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落地难其实一开始就没把战略设计这一步做扎实。我在实际项目中最大的体会是当团队把精力投入在建立统一语言、划清限界上下文、认清核心域这些看似“不写代码”的事情上时后面写代码的速度反而会变快因为方向对了所有的力气都不会白费。