ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

限界上下文与聚合:解决业务逻辑混乱的DDD核心实践

限界上下文与聚合:解决业务逻辑混乱的DDD核心实践 做了快十年后端我接手过一个订单系统代码量不大但每次需求评审我都有种拆弹的错觉订单状态判断逻辑散落在六个Service里下单流程里七处都在问“订单是不是已支付”而每个模块对“已支付”的理解还不完全一样。营销部说的订单金额和财务部说的订单金额能差出一张满减券来。那段时间我几乎每天都在回答同一个问题——这段业务逻辑到底该放哪。后来重读Eric Evans的《领域驱动设计软件核心复杂性应对之道》才发现我们缺的不是更细的微服务拆分也不是换个ORM而是一个能统一团队认知、把业务规则重新拉回代码中心的建模纪律。严格说这本书写于2003年但直到今天它依然是处理软件核心复杂性的最系统、最完整的一本方法论。我读了三遍每遍的收获都不同。第一遍学概念第二遍学边界第三遍读到的其实是沟通和建模的底层逻辑。这篇文章就当作一份读后总结与思考聊聊这本书到底讲清楚了什么、我们在实践中容易在哪儿翻车以及一套我验证过、可以渐进落地的路径。1. 为什么软件会失控以及这本书的靶心在哪要理解这本书的价值得先弄清一个前提软件系统的复杂度到底从哪来。很多人第一反应是“功能太多”“历史包袱重”“团队协作乱”但Fred Brooks早就区分过两类复杂度一类是偶然复杂度来自我们用的工具、语言、框架以及糟糕的工程实践另一类是本质复杂度来自业务领域本身固有的逻辑。Eric Evans的靶心正是后者。1.1 领域逻辑的隐性化是失序的开始我见过太多系统业务规则没有被建模而是散落在无数个Service和工具类里。团队每天都在用代码“表演”业务但代码本身没有一个稳定的领域模型来承载这些规则。你问“一个订单什么时候算成立”十个人能给你十种回答代码里的答案只能是if-else数量的叠加。这不是管理问题是建模问题。领域驱动设计在做的是让代码里的每一个对象、每一次方法调用都能映射到领域专家脑子里那个真实业务概念上去。当代码模型与业务模型发生漂移软件就开始腐烂。这本书反复强调的核心主张就是模型不是文档里的UML图模型是代码与业务共识的统一体。1.2 一个真实的复杂度失控信号回过头看那个订单系统失控的信号非常明显领域名词的含义模糊——“订单”“客户”“支付”在不同模块里含义不一致。规则实现重复且互相矛盾——“改一崩三”成了家常便饭。没有人能说清完整业务链路——大家只知道自己负责的那一亩三分地。这些问题不用DDD也能修但没有DDD的视角你修完A处B处又会冒出来。因为本质复杂度没有被正面管理它只是换了个地方继续膨胀。1.3 这本书到底适合谁来读如果你负责的是几十行代码的脚本或者纯粹的CRUD后台这本书对你来说就是屠龙术没必要硬啃。但如果你在一个业务逻辑密集的系统里写过两年以上的代码或者作为一个技术Leader需要为系统的长期演化负责那这本书几乎是必修课。它不是一本教你怎么写代码的工具书而是一本教你怎么和组织一起应对复杂业务的方法论。带着“我正在维护一个复杂业务系统”的场景去读你会越读越觉得它在说你手头的项目。2. 限界上下文全书最值钱的战略级概念市面上讲DDD的文章十个有九个在讲实体、值对象、聚合但真正让这本书封神的是战略设计部分尤其是“限界上下文”这个思想。2.1 “一个全局统一模型”是个危险的幻觉我最早学DDD的时候有个特别天真的想法把整个公司的所有业务统一建模成一个巨大的领域模型大家共享同一套实体定义那多完美。书里劈头盖脸地告诉我这个想法有多危险——在真实商业世界里不同业务场景对同一个名词的理解天然就不可能完全一致。举个最常见的例子“客户”。在销售部门的上下文里客户是“正在跟进、有成交意向的线索”在财务部门的上下文里客户是“有账期、有应收账款的往来单位”。这两个客户能合并成一个对象吗硬合并的结果就是对象里堆满无关字段每个团队只用其中一小部分最终变成一个谁也不敢动的怪兽类。限界上下文的核心思想是**承认模型是有边界的每个边界内部保持模型一致边界之间通过明确的方式集成。**这个思想听起来简单但执行起来需要很强的自律——因为几乎所有人都有“统一一切”的本能冲动。2.2 用上下文映射图管理模型间的关系边界划出来之后模型与模型之间怎么协作书中给出的核心工具是上下文映射图Context Map。它不是我当初理解的“模块依赖图”而是一张用组织语言绘制的地图标记出每个限界上下文之间到底是什么关系。最常用的几类关系防腐层ACL下游模型不想被上游模型污染时在中间做一层转换。这是我最常用、也最推荐优先采用的关系。共享内核Shared Kernel两个团队共享一小部分公共模型但必须严格控制变更频率。客户-供应商Customer-Supplier上游团队和下游团队是供应关系接口变化需要协商。发布语言Published Language通过一套公开的、稳定的协议比如事件Schema来交换信息。举一个实际例子。我们订单系统和外部积分系统对接时积分系统里的“订单”概念跟我们的核心订单模型严重不一致。如果我们直接把对方的字段塞进自己的聚合里订单模型会被污染。最后用防腐层在边界处做了翻译内部保持纯净的订单模型外部对接的变化被完全隔离在防腐层内部。这个结构持续用了很久后来积分系统改了两版接口核心领域代码一行都没动。2.3 子域划分核心域、支撑子域、通用子域限界上下文解决的是“边界怎么划”而子域分析回答的是“精力往哪投”。书里把业务域分成三类核心域Core Domain系统最核心、最独特的竞争力是必须投入最优秀人才和最充分建模精力的地方。支撑子域Supporting Subdomain支持核心业务但不是核心差异点比如订单履约里的仓储管理。通用子域Generic Subdomain到处都在用的通用能力比如权限、消息通知、用户认证优先考虑现成方案而不是自己造轮子。这个分类对我的冲击很大。因为绝大多数团队的实际做法正好相反把大量人力花在权限系统、消息中间件、基础组件的“自研”上而自己真正的核心业务——那套能赚到钱的业务规则——却拖着一个贫血模型勉强运行。用书里的话说你得先搞清楚什么才是你系统的“心脏”然后围着心脏全力以赴。这不是技术问题而是战略取舍问题。如果核心域得不到最好的建模资源系统最终会输给竞争对手哪怕你的基础设施再光鲜。3. 战术建模工具箱实体、值对象、聚合到底该怎么用如果说战略设计是画地图那战术建模就是具体修路。书里后半部分给了一套完整的建模工具集这些概念网上讲得很多但真正用对的人少。我重点说几个我踩过坑或者观察过团队踩坑的地方。3.1 实体和值对象判断标准不是“有没有ID”很多人判断一个东西是实体还是值对象就看它有没有主键ID。这个判断题在数据库表层面没问题但放到领域模型里就会错得离谱。书里的判断标准是这个对象在业务中是否需要被持续追踪其身份变化“人”是实体。同一个人的年龄、地址变了他还是他因为身份没变。“金额”是值对象。100块钱和另一笔100块钱如果都是人民币面额它们没有独立身份彼此可以互换。订单配送地址是值对象。修改地址不是“把原来的地址更新了”而是“用一个新的地址对象替换了旧的”这在建模时是完全两种思路。这个区分对于一个建模者不是掉书袋它的直接收益是你开始学会用值对象去消灭setter用不可变对象去表达不变量。一个领域对象如果全是可变字段谁都说不清哪些变化是被允许的哪些是被禁止的。值对象一旦创建就不可变天然解决了这个复杂性。3.2 聚合一致性边界不是越大越好也不是越小越好聚合是全书战术部分最难掌握、也最容易走极端的概念。聚合要解决的问题是一组对象之间存在业务不变量invariant这个不变量的维护必须在一个事务边界内完成。聚合根是唯一的外部访问入口外部对象只能通过聚合根方法修改聚合内部状态。经典例子是“订单-订单行”。订单的总额必须等于所有订单行金额之和这个不变量要求“添加订单行”和“重算订单总额”必须在同一个事务里完成。这时候订单就是一个聚合订单行是聚合内部的实体外部不能直接操作订单行。但我见过两个极端上帝聚合把订单、支付、物流、退款全部揉进一个大聚合。结果是并发冲突严重一个聚合上动不动就加锁性能惨不忍睹。现实中我见过有人把“订单-用户-优惠券-库存”做成一个聚合理由是它们业务上都有关系这基本是灾难。零聚合把订单拆得到处都是一个操作要同步修改好几个聚合然后靠分布式事务硬凑。这等于放弃了聚合带来的一致性保证又回到了散落逻辑的老路。正确做法是不断问这个业务不变量到底需要多强的即时一致性如果只是最终一致那就不该硬塞进一个聚合如果是强一致那就老老实实控制边界。聚合大小的拿捏本质上是业务一致性与系统性能之间的权衡没有标准答案只有针对业务规则的判断。3.3 领域服务与应用服务把业务逻辑从Service中救出来这是我看过最多团队踩坑的地方。很多人把代码全堆在Service里然后宣称自己用了DDD——因为Service里有实体、值对象和仓储。其实那只是“贫血模型大Service”跟DDD关系不大。书里的逻辑是分层的应用服务负责用例的流程编排、事务管理、权限控制。它不应该承载业务规则它只负责“协调”。领域服务当某个业务动作无法自然地归属到某个实体或值对象上时把它建模成领域服务。它表达的是领域中的动作或过程。实体/值对象承载业务规则和状态变化。举一个例子“优惠分摊计算”。在退款场景里一笔订单包含多个商品用了多张优惠券退款时需要按比例把优惠金额分摊到每个商品上。这个计算逻辑放哪个实体里都不自然放应用服务里又会把领域规则和流程编排混在一起。正确的做法是建模一个RefundProportionCalculator领域服务它只知道优惠规则和分摊比例不知道数据库、不知道事务。判断职责是否混淆有个笨办法看那段代码里有没有出现Repository.Save、Transaction.Commit这类基础设施词汇。如果有你大概率坐在应用服务里。领域服务应该只跟领域概念打交道。3.4 领域事件不是消息队列是模型的一部分领域事件在书里被单独放在一章我当时读第一遍时把它当成“用MQ解耦”来理解后来才明白完全不是一回事。领域事件建模的关键是**在业务流程中确实发生了一件领域专家认为很重要、后续可能有其他地方关心的事。**比如“订单已提交”“订单已支付”“库存已预留失败”。当你从领域模型里提炼出这些事件你的模型就从“命令式”变成了“事件驱动式”聚合之间的协作不再直接调用对方而是发布事件由订阅方决定怎么响应。这里有一个非常常见的误区把领域事件和消息队列直接画等号。有人拿到事件驱动架构的Kafka就把系统里能发消息的地方全部改成事件但事件名只是简单复制了方法名并没有认真思考“这是不是一件领域里真实发生的事”。结果换来一堆难以追踪的隐式调用。正确做法先回到领域把你业务中的“过去分词”找出来——已支付、已审核、已发货、已取消——这些才是候选领域事件。消息队列只是事件的传输载体真正宝贵的是事件所承载的领域语义。4. 三个翻车现场以及一条可行的渐进落地路径理论讲完聊聊我观察到的真实落地情况。DDD在实践中的失败率其实不低但绝大多数失败不是理论错了而是落地姿势错了。搞定这三个典型的翻车现场基本能避掉一半的坑。4.1 翻车现场一没有战略设计直接开工战术建模有些团队看了“实体、值对象、聚合”的教程很兴奋立刻拉上开发开始画聚合。几周后设计出一堆边界模糊、关系复杂的聚合代码已经是“DDD风格”了但大家依然说不清每个聚合存在的理由。根因就是跳过了战略设计。限界上下文都没搞清楚战术建模做得再漂亮也是一盘散沙。聚合是限界上下文内部的一致性边界边界之外还有翻译和集成的问题没有解决直接深入细节注定失败。务实的做法是任何DDD落地从限界上下文和上下文映射图开始先画对地图再谈盖楼。4.2 翻车现场二照搬别人的聚合设计DDD有个迷惑性很强的地方——访问“订单聚合”是业界常青话题所以很容易在网上找到各种论文级的订单模型。于是有人直接抄过来以为自己拿到了通用解。但聚合的边界本质上是业务规则边界而每个公司的业务规则都不一样。A公司的订单可以合并付款B公司不允许C公司的订单支持多次退款D公司只支持整单退款。这些差异会让聚合的边界和内部结构完全不同。别人的聚合再好没有你的业务规则喂给它它就是个空壳。正确用法是把书里的案例、公开的建模当作思维训练但最终聚合边界必须由你和你领域专家坐下来对着自己的业务流程一刀一刀切出来。4.3 翻车现场三通用语言只存在于墙上的白板通用语言Ubiquitous Language是DDD最基础、也最容易被忽略的一环。很多团队启动DDD时很兴奋工作坊里做了术语对齐贴了一墙白板过两周就没人看了。代码里的类名叫Order业务人员嘴里说的是Trade测试用例里又用一个PurchaseOrder三个词汇指同一个东西没人发现这是件大事。通用语言要是不能进入代码、测试、日常讨论和需求文档它就只是一张废纸。而真正落地通用语言的团队会在代码评审时指出“这个变量名和领域术语不一致”会发现产品经理写需求时开始用和你代码相同的词。语言统一了模型才可能统一。想验证你们的通用语言是否失效有个简单的自查办法随便写一个业务异常日志看看开发、产品、测试三个人读完之后是否能用完全相同的词汇复述发生了什么。4.4 不适合DDD的场景也值得明说DDD不是万能药它只适合解决真正的业务复杂性。以下的场景硬上DDD大概率得不偿失纯CRUD系统业务规则非常薄改成啥样都差不多。团队无法接触到真正的领域专家只能靠猜来建模。业务在快速变化没人敢承诺模型的稳定性所有设计都处于“随时推翻”状态。系统本身不存在高复杂度只是团队想追赶技术时髦。这里不是劝退而是说工具要匹配问题。就像不会有人用战略规划去安排一次楼下便利店采购复杂业务才值得引入DDD这个级别的思考工具。4.5 一条实际验证过的渐进路径如果你读完这本书觉得“确实有用”又不知道从哪下手我给你一条不需要大动干戈的路径选一个业务复杂度高、影响面可控的垂直切片而不是从整个系统开始。组织一次事件风暴或业务建模工作坊让开发和业务人员围在一起把核心流程完整走一遍。画出限界上下文明确哪个是核心域哪个是支撑域先只关注核心域。在核心域内部做战术建模识别实体、值对象、聚合根。用防腐层隔离外部系统对你核心模型的干扰。新模块用这套方法跑起来验证顺畅后再逐步扩展不要动老系统不要一次学完所有概念再动手。我发现渐进式最大的好处是团队不需要先变成DDD理论专家就能开始干活边做边理解书里的概念落地阻力会小很多。5. 重读之后我对建模这件事的几点新认识书读多了概念会模糊但几条底层认知反而越来越清晰。到最后一章我想说的是这本书对我工作方式最深层的影响。5.1 模型不是从需求里“提取”出来的是设计出来的很多人一想到领域建模就觉得是把业务专家的描述自动翻译成UML图。读了书之后我才明白模型从来不是需求的副产品而是建模者与领域专家共同设计出来的抽象。领域专家知道自己要什么但不知道代码怎么组织才不腐烂开发知道代码怎么组织但容易脱离业务本质。好的模型必须由双方一起碰撞出来这需要持续的对话与迭代而不是一个文档评审会就能搞定。5.2 代码就是模型的载体模型是会演化的活物这是这本书和很多“设计文档方法论”最本质的区别**模型必须绑定到实现上。**如果模型只存在于PPT里代码和模型迟早会分道扬镳。但反过来只要代码还在跑模型就会随着对业务理解的加深、业务规则的改变而持续演化。所以我不再追去“画一张完美无缺的图”再开工而是设计一个足够好的最小模型让代码先跑起来在演进过程中不断重构模型。这听起来比“大设计先行”缺了点安全感但做久了你会发现这才是对抗复杂性的真实姿态——持续的小步重构胜过一次性的宏大蓝图。5.3 限界上下文思想比微服务工具链更重要这几年微服务很流行很多人讨论服务拆分时谈的是容量、团队结构、框架特性唯独没谈过领域边界。我后来发现限界上下文正好补上了这块拼图——它回答的是“服务边界到底应该划在哪”这个问题。如果你先有清晰的限界上下文微服务拆分只是水到渠成的事但如果你没有战略设计直接按表拆服务拆出来的服务大概率是从二楼的窗户搬石头依然是乱的。很多微服务项目失败不是框架不够好而是边界本身就错了。5.4 CQRS、事件溯源和DDD不是绑定关系最后说一个被媒体反复炒作的关系。看到网上很多文章把DDD、CQRS、事件溯源Event Sourcing打包成一套“终极方案”仿佛不用CQRS就不配叫DDD。书里确实提过读写模型分离的思路但那是作为高阶复杂场景的选项之一不是标准配置。我的判断是如果你的系统不存在显著的读写不对等、没有复杂的历史追溯需求老老实实用传统CRUD加领域模型就够了。每多引入一个工具系统复杂度就多一层。让工具匹配问题而不是反过来让问题去迁就工具组合。读了这么多年我最大的体会是这本书不是读完就完事的它是用来反复对照现实问题的。每当我发现自己在一个系统里改代码开始“不确定这逻辑应该放哪”时我就会翻一翻对应的章节答案通常就在某一段被我画过线的句子里。下一次如果你也遇到一台代码跑得动、但没人说得清模型是什么的系统我建议你找个周末把这本书拿出来慢慢读。
返回列表