☰
领域驱动设计DDD:通用语言、限界上下文与聚合根实践
2026/11/2 8:46:19 网站建设 项目流程

“订单”这个词,我在三个不同团队的系统里见过三种完全不同的定义:销售团队说的订单是一份意向确认书,履约团队说的订单是一张拣货发货指令,财务团队说的订单是一笔应收凭证。三个系统各有一套字段、各有一套状态机,接口对接时字段名一样、语义全不一样,改一个“订单状态”能让三个团队开三次会。这种场景见得太多了,最后大家往往得出同一个结论:问题不在代码写得好不好,而在于整个组织对“订单”这个词没有共识,代码只是把这个分歧忠实地记录了下来。领域驱动设计(DDD)真正要解决的,就是这类由业务复杂度带来的问题——它不是一套分层模板,也不是一堆设计模式的拼盘,而是一套让业务语言、系统边界和代码模型三者对齐的方法论。这篇笔记整理的是DDD最基础的那层理论与概念:通用语言、限界上下文、上下文映射、实体与值对象、聚合与聚合根、领域服务与领域事件、分层与依赖。适合已经写过几年业务代码、被复杂业务规则折磨过、想系统梳理一遍DDD底层概念的人看;如果你手上只是几个字段的增删改查,坦率讲,DDD 带来的是负担而不是收益。

1. 先搞清楚DDD的适用边界:它治的是业务复杂度,不是技术复杂度

我见过不少团队在技术选型会上把DDD搬出来,理由五花八门,比如“微服务要拆,所以得上DDD”“代码太乱了,用DDD重构一下”。这种出发点通常撑不过三个月。DDD 的原始定位非常明确:它针对的是业务领域的复杂性,而不是并发、性能、分布式事务这类技术复杂性。这两类复杂性需要的是完全不同的工具,把DDD当成技术复杂度的手术刀,结果往往是引入了一堆新概念,业务规则反而更散。

1.1 业务复杂度的三个典型信号

判断一个系统到底需不需要DDD,我一般看三个信号,出现两个以上才值得投入。

第一个信号是业务规则的数量和变化频率。比如计价规则,不是简单的“单价乘数量”,而是会员等级、促销叠加、税费计算、阶梯折扣、退款冲抵互相纠缠,而且每隔一两个月就有新的营销活动要接进去。这种规则一旦用 if-else 堆起来,半年之后就没人敢动了。

第二个信号是同一业务概念在不同部门之间语义分裂。开头说的“订单”就是典型。当同一个词在不同上下文里含义不同,而这几个上下文又要频繁协作时,光靠开会同步是治不好的,必须有明确的边界和翻译机制。

第三个信号是多人长期协作带来的模型腐蚀。一个模块从三个人维护变成十五个人维护,每个人按自己的理解往里面加字段、加状态,两年后这个模型的原始设计意图已经完全看不出来了。这时候缺的不是重构,而是模型本身的表达力。

反过来看,如果业务就是“表单提交、存库、列表展示”,规则少、变化慢、单人维护,那么用DDD反而会制造出大量空壳类:一个只有 getter/setter 的实体、一个只做透传的领域服务、一个永远只有 findById 和 save 的仓储。这些类不减复杂度,只增加导航成本。

1.2 战略设计与战术设计不是一回事

这是初学者最容易混淆的地方,我自己也在这个坑里待过很久。DDD 分成两层:

战略设计管的是“系统怎么切分”。它包括通用语言、限界上下文、上下文映射、子域划分(核心域、支撑域、通用域)。这一层决定了团队边界和系统边界,投入产出比最高,但恰恰是最容易被跳过的一层。

战术设计管的是“一个边界内部怎么建模”。它包括实体、值对象、聚合、聚合根、领域服务、领域事件、仓储、工厂。这一层是具体写代码时的工具箱。

我踩过的坑是:直接抱着《领域驱动设计》啃战术模式,把实体、值对象、聚合全用上了,代码看起来特别“DDD”,但系统边界一塌糊涂,三个上下文共用一套实体类,最后领域层变成了一个巨型共享库。后来才明白,战略设计错了,战术设计做得再漂亮也只是给混乱加了一层精致的外壳。反过来,战略设计做对了,战术模式用得粗糙一点,系统依然能长期演进。

提醒:如果你的团队连“某几个业务概念归谁负责”都还没谈清楚,就先别急着画聚合、写领域事件,那是把顺序搞反了。

2. 通用语言:先让所有人说同一种话,再谈写同一种代码

通用语言(Ubiquitous Language)是DDD里最朴素也最难落地的概念。它的定义很简单:在一个限界上下文内,业务人员、产品、开发、测试使用同一套词汇,并且这套词汇直接体现在代码命名、数据库字段、接口文档、测试用例里。听起来是常识,但实际操作起来阻力极大,因为大部分团队的默认状态是“业务说业务的黑话,开发说开发的黑话,中间靠产品经理做人工翻译”。

2.1 通用语言失效的三种典型形态

第一种是文档型通用语言。团队认认真真搞了一次术语梳理,产出一份三十页的词汇表放在知识库里,然后就没有然后了。代码里依然是status、type、flag、kind混用,词汇表变成了一份没人看的考古资料。通用语言不是文档,是活的约定,它必须出现在变量名、方法名、类名、接口路径、甚至聊天记录里。

第二种是翻译层型通用语言。开发很努力地把业务词汇翻译成自己习惯的技术词汇,比如业务说“授信额度”,代码里叫creditLimit,业务说“占用”,代码里叫lock,业务说“释放”,代码里叫unlock。每个词单独看都合理,但业务人员看代码完全看不懂,开发看需求文档也要在心里做一次映射。映射本身不难,难的是映射规则没人维护,新人进来全靠口口相传,半年后lock和freeze就混着用了。

第三种是跨上下文强统一。有人觉得通用语言就该全公司统一,于是把“客户”这个词强行统一成一个类。结果销售上下文里的客户是“有购买意向的联系人”,结算上下文里的客户是“有信用账户的法人主体”,客服上下文里的客户是“有历史工单的服务对象”。强行合并之后,这个类膨胀到八十多个字段,每个上下文只用到其中一小部分,字段含义还得靠注释解释。这就是典型的把“通用语言”误解成了“全局语言”。

2.2 让通用语言真正长进代码里的四个动作

动作一:从事件风暴开始收集词汇,而不是从需求文档开始。需求文档往往是经过加工的,事件风暴现场业务人员脱口而出的词才是真词汇。做法很简单:把业务方拉到一块白板前,让他们按时间顺序讲一遍业务流程,每个动作写一张便签,每个名词写一张便签。你会发现很多同义词(“下单”和“创建订单”其实是同一件事)和伪同义词(“取消”和“关闭”其实差别很大)。把这些分歧当场消掉,比事后开会高效得多。

动作二:建立“词—代码”对照表,并且纳入代码评审。我们当时做法是在项目根目录放一个GLOSSARY.md,每个词条包含:业务定义、所属限界上下文、代码中的类名/方法名/字段名、反例(哪些写法是禁止的)。代码评审时,如果 PR 里出现了词汇表之外的业务概念命名,评审人有权要求改名。这个规则执行了三个月之后,命名一致性明显好转。

动作三:把通用语言写进测试用例的方法名。这一点特别有效但常被忽略。测试方法名用业务语言描述,比如下单时会员折扣与优惠券不可叠加(),比testCreateOrder3()有用一百倍。测试名是最贴近业务行为的代码文本,用它来锚定通用语言,成本极低。

动作四:允许上下文内部分裂,但在边界上强制翻译。不同上下文对同一个词有不同定义是正常的,甚至是必要的。关键是不能让两套定义直接碰撞。跨上下文调用时,必须经过一层显式的翻译,这个翻译在战术层面对应防腐层,在战略层面对应上下文映射里的“客户方—供应方”关系。

失效形态表面现象根本原因修正动作
文档型词汇表躺在知识库里词汇未进入代码把词表纳入代码评审
翻译层型业务看不懂代码,开发看不懂需求缺少双向映射约定建立词—代码对照表
跨上下文强统一单个类字段爆炸误把通用语言当全局语言按上下文拆分同名概念

2.3 一个小的实践样本

举个我们做过的例子。业务方反复说“额度占用”和“额度冻结”,我们一开始以为是同义词,合并成了freeze。后来在事件风暴里追问细节才发现:占用是下单时临时扣减,超时自动释放;冻结是风控介入后的强制限制,必须人工解冻。这两个行为的生命周期、触发方、释放条件完全不同。后来拆成了reserve(占用)和freeze(冻结)两个方法,业务方看代码时一眼就能认出来,测试用例也直接按这两个词组织。这件事让我确信:通用语言的价值不在于统一,而在于精确。

3. 限界上下文与上下文映射:给混沌的业务划出一条条可维护的缝

限界上下文(Bounded Context)是DDD战略设计的核心,它可以理解为“一个模型适用的边界”。在这个边界内,某个业务概念有唯一确定的含义;跨出边界,同一个词可以有不同的含义,而且不需要协调。这个定义听起来抽象,落地时其实很好判断:如果一个团队能独立决定某个概念的字段和规则,不需要和别的团队同步,那它就是一个限界上下文。

3.1 划分限界上下文的四个现实依据

教科书上常说要按“业务能力”划分,这个说法太虚。我在实际项目里主要看四个现实依据:

依据一:语言边界。如果两个团队对同一个词的争吵超过两次,基本可以判定它们属于不同上下文。语言分裂是最可靠的信号,因为它反映的是真实的业务认知差异,而不是组织结构的一时变动。

依据二:变化频率。两个模块的发布节奏差异很大,一个每周发版,一个每季度发版,强行放在一个上下文里会互相拖累。变动频率不同的部分,边界往往也不同。

依据三:数据一致性要求。需要强一致的放在同一个聚合甚至同一个上下文里;可以接受最终一致的,就可以拆成不同上下文,通过事件协作。这里要小心,很多业务方嘴上说“必须强一致”,追问下去其实“几秒内一致”就能接受。

依据四:团队规模与沟通成本。一个上下文最好由一个能坐在一起、能随时沟通的小团队负责。团队规模超过十几个人,沟通成本会指数级上升,这时候要么拆上下文,要么拆团队,反正得拆一样。

需要注意的是,限界上下文不等于微服务。一个上下文可以是一个模块、一个进程内的包,也可以是多个服务;反过来,一个微服务里塞三个上下文也很常见。把两者划等号,会逼着团队在业务边界还没想清楚的时候就去做分布式拆分,那是灾难。

3.2 上下文映射的几种关系与选型逻辑

上下文划分出来之后,它们之间必然要协作,协作方式就是上下文映射(Context Mapping)。常见的关系有几种,我按推荐程度从高到低排一下:

防腐层(Anticorruption Layer)是我最推荐的一种。做法是在自己的上下文里定义一套符合本地模型的接口,外部上下文的数据进来时先经过一层转换器,把外部模型翻译成本地模型。好处是外部模型变化时,只需要改转换器,本地领域模型不受污染。代价是多写一层映射代码。我的经验是:只要对接的是外部系统、老旧系统或者第三方服务,防腐层几乎是必选项,省下的这层代码迟早会以更大的代价还回来。

客户方—供应方(Customer/Supplier)适合上下游团队地位对等、能协商的场景。下游是客户,上游是供应方,双方约定接口契约,上游变更前通知下游。这需要一定的组织成熟度,否则容易变成上游单方面改接口、下游被动救火。

开放主机服务(Open Host Service)+ 发布语言(Published Language)适合一个上下文要对接多个下游的场景。上游定义一套稳定的对外协议(比如 REST 接口加 JSON Schema),所有下游按这套协议对接。这本质上是在上下文边界上做了一层 API 网关式的抽象。

遵奉者(Conformist)是下游完全跟随上游模型,不做翻译。省事,但代价是本地模型被外部模型绑架。只有在外部模型足够稳定、且本地业务确实没有独立表达需求时才考虑。

共享内核(Shared Kernel)要极其谨慎。两个上下文共享一小部分模型代码,看似省事,实际上把两个团队的发布绑在了一起。一旦共享部分要改,两边都得动。我的建议是共享内核越小越好,最好只共享最基础的值对象,比如金额、货币单位。

各行其道(Separate Ways)常常被低估。如果两个上下文之间没有实质协作需求,就干脆不要建立关系,各做各的。很多所谓的“集成需求”其实是组织政治的产物,不是业务需要。

映射关系适用场景主要代价我的默认选择
防腐层对接外部/遗留系统多一层转换代码外部依赖首选
客户方—供应方内部上下游团队依赖组织协作成熟度内部上下游常用
开放主机服务一对多对接上游需维护稳定契约平台型上下文常用
遵奉者外部模型稳定且本地无独立表达本地模型被绑架慎用
共享内核极少量基础类型共享发布耦合越小越好
各行其道无实质协作无优先考虑

4. 战术设计的四块积木:实体、值对象、聚合、领域服务

战略设计定完边界,才轮到战术设计。这一层最容易出现的问题不是“不会用”,而是“用过头”——把每个类都硬套成实体或值对象,结果模型比业务本身还复杂。我的体会是:战术模式是工具箱,不是清单,不需要每个都用上。

4.1 实体与值对象:判别标准只有一个

实体(Entity)和值对象(Value Object)的区别,教科书上说“实体有标识,值对象没有”,这个说法正确但不够用。我用的判别标准更实际:这个东西换一个实例,业务上是否等价?

举个例子。一笔转账记录里的“金额”,100 元和另一笔 100 元在业务上是完全等价的,谁拿着这 100 元不重要,重要的是数值本身,所以金额是值对象。而“账户”就不一样了,账户 A 和账户 B 就算余额、类型、开户时间全都一样,也是两个不同的账户,因为有账号这个标识,所以账户是实体。

值对象的关键特征是不可变。一旦创建就不能修改,要改就整体替换。这个约束很多人不适应,觉得每次改个字段都要 new 一个新对象很麻烦。但正是这个约束带来了巨大的好处:值对象可以安全地在多线程间传递、可以放心地做缓存、可以自由地做相等性比较,不用担心被某个调用方偷偷改掉。

// 值对象:不可变,整体替换,相等性由属性决定 public final class Money { private final BigDecimal amount; private final String currency; public Money(BigDecimal amount, String currency) { if (amount == null || currency == null) { throw new IllegalArgumentException("金额与币种不可为空"); } if (amount.scale() > 2) { throw new IllegalArgumentException("金额精度不得超过两位小数"); } this.amount = amount; this.currency = currency; } public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("币种不一致,无法相加"); } return new Money(this.amount.add(other.amount), this.currency); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money)) return false; Money money = (Money) o; return amount.compareTo(money.amount) == 0 && currency.equals(money.currency); } @Override public int hashCode() { return Objects.hash(amount.stripTrailingZeros(), currency); } }

这段代码里有几个细节值得说。第一,构造函数里做了校验,非法状态根本创建不出来,这比事后到处判断要可靠得多。第二,add返回新对象而不是修改自身,保证了不可变性。第三,equals用compareTo而不是equals比较金额,是为了处理100和100.00这种精度差异——这个坑我在实际项目里踩过,两个金额明明相等,用equals判断却是 false,查了半天才发现是BigDecimal的 scale 不同。

实体这边要注意的是标识的生成时机。很多项目习惯用数据库自增 ID 作为实体标识,这在DDD里是有问题的:实体在入库前就应该有标识,否则领域层就没法判断两个对象是不是同一个。常见的做法是在领域层用 UUID 或者雪花算法生成标识,数据库主键只是持久化细节。

4.2 聚合与聚合根:一致性边界,不是对象树

聚合(Aggregate)是DDD里最容易被误解的概念。很多人把它理解成“有包含关系的对象组合”,于是设计出巨大的对象树,一个订单聚合里塞进订单项、支付记录、物流信息、评价、优惠券……最后这个聚合加载一次要查七八张表,保存一次要处理一堆并发冲突。

聚合的本质是一致性边界和事务边界。它的定义是:一组相关对象,作为一个整体进行数据修改,并且必须保证在事务结束后,聚合内部的所有不变量都成立。这句话里有两个关键词:

关键词一:不变量。聚合内部必须始终满足的业务规则。比如“订单总金额等于所有订单项金额之和”“订单项数量不能为负”。这些规则必须在聚合内部保证,不能依赖外部定时任务去修。

关键词二:事务边界。一次事务只修改一个聚合。跨聚合的操作靠领域事件做最终一致,而不是靠一个大事务包住。

基于这两点,聚合设计有几个硬性原则:

  • 聚合根是外部访问的唯一入口,外部不能直接持有聚合内部对象的引用;
  • 聚合尽量小,只把必须同时满足不变量的对象放进来;
  • 聚合之间通过标识引用,而不是对象引用;
  • 一个事务只改一个聚合。

拿订单举例。订单和订单项放一起是合理的,因为“总金额等于各项之和”这个不变量必须同时保证。但支付记录就不该放进来,支付有自己的生命周期和状态机,它和订单之间是最终一致的关系,应该独立成聚合,通过领域事件通知订单更新状态。

// 聚合根:订单,内部维护不变量,外部只能通过聚合根访问 public class Order { private final OrderId id; private final List<OrderItem> items = new ArrayList<>(); private OrderStatus status; public Order(OrderId id) { this.id = id; this.status = OrderStatus.DRAFT; } public void addItem(ProductId productId, Money unitPrice, int quantity) { if (status != OrderStatus.DRAFT) { throw new IllegalStateException("非草稿状态的订单不可追加条目"); } if (quantity <= 0) { throw new IllegalArgumentException("条目数量必须为正"); } items.add(new OrderItem(productId, unitPrice, quantity)); } public Money totalAmount() { return items.stream() .map(OrderItem::subtotal) .reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add); } public void submit() { if (items.isEmpty()) { throw new IllegalStateException("空订单不可提交"); } this.status = OrderStatus.SUBMITTED; } public List<OrderItem> items() { return Collections.unmodifiableList(items); } }

注意items()返回的是不可变视图,外部拿不到内部集合的修改权限,所有变更必须走addItem这类聚合根方法。这是保证不变量的前提,如果对外暴露了可变集合,不变量随时可能被绕过。

4.3 领域服务、领域事件与仓储:什么时候该用什么

领域服务(Domain Service)用于承载“不属于任何单一实体或值对象的业务行为”。典型场景是跨聚合的规则,比如“转账时校验转出方余额是否充足并扣减,同时增加转入方余额”,这个行为同时涉及两个账户聚合。这里要注意,领域服务不是“把实体搞不定的逻辑全丢进去”的垃圾桶,判断标准是:这个行为是否真的不属于某一个聚合根。如果属于,就应该写成聚合根的方法。

领域事件(Domain Event)表达“领域中发生的、其他部分可能关心的事实”,命名用过去式,比如OrderSubmitted、PaymentSettled。它的价值在于解耦:下单之后要做的库存预占、积分计算、消息推送,都不需要在订单聚合里显式调用,发布事件即可。我踩过的坑是把领域事件直接当消息队列用,在事件里塞了大量的跨系统调用逻辑,结果领域层依赖了中间件客户端,单元测试跑不起来。正确做法是领域事件只在领域层内部发布,由应用层或基础设施层负责转发到外部消息系统。

仓储(Repository)是聚合的持久化抽象,注意几个边界:一个聚合对应一个仓储;仓储接口定义在领域层,实现在基础设施层;仓储只处理聚合根,不处理聚合内部的非根对象;仓储不是 DAO,不应该有findByStatusAndCreateTimeBetween这种查询方法无限膨胀,复杂查询应该走专门的查询服务或者 CQRS 的读模型。

5. 分层与依赖:DDD的代码结构到底长什么样

概念都清楚了之后,下一个问题就是“代码怎么组织”。这块最容易形式化——把包名改成domain、application、infrastructure就宣称自己用了DDD,实际依赖关系还是乱的。

5.1 四层架构的依赖方向是核心

经典的四层是:用户界面层(或接入层)、应用层、领域层、基础设施层。关键不在分成几层,而在于依赖方向:领域层必须是最纯粹的一层,不依赖任何外部框架、数据库、消息队列;基础设施层负责实现领域层定义的接口(依赖倒置)。

层次职责允许依赖典型内容
接入层协议适配、参数校验、鉴权应用层Controller、RPC 入口
应用层用例编排、事务边界、权限领域层ApplicationService
领域层业务规则、状态流转无(仅语言基础库)实体、值对象、聚合、领域服务
基础设施层持久化、消息、外部调用领域层接口Repository 实现、事件发布器

很多项目的领域层里出现了@Entity、@Table这类 ORM 注解,或者直接 import 了某个 HTTP 客户端,这就是依赖方向被破坏了。短期看不出问题,长期后果是领域模型和数据库表结构死死绑在一起,业务想换个存储方式或者写个纯内存的单元测试都做不到。

应用服务(Application Service)和领域服务(Domain Service)的区别也常被搞混。应用服务负责“编排”——接收请求、加载聚合、调用领域方法、保存聚合、发布事件、处理事务;它本身不应该包含业务规则。领域服务则包含业务规则,但不处理事务和持久化。判断标准很简单:如果一段代码里出现了@Transactional,它大概就是应用服务;如果出现了业务判断,它大概属于领域层。

5.2 从贫血模型迁移到充血模型的可行路径

绝大多数存量项目都是贫血模型:实体只有字段和 getter/setter,业务逻辑全在 Service 里。直接推翻重写风险太大,我实践过的渐进路径是这样的:

第一步,先把值对象抽出来。这一步风险最低、收益最明显。金额、手机号、身份证号、地址这些概念,从裸字段抽成不可变的值对象,校验逻辑收进构造函数。这一步不改变整体架构,但能立刻消灭大量重复校验代码。

第二步,把最稳定的业务规则搬进实体。挑那些规则简单、调用点集中的方法,比如订单状态流转、库存扣减校验,从 Service 里搬进聚合根。搬运时保持 Service 方法签名不变,内部改为委托给聚合根,这样调用方无感知。

第三步,识别并收敛聚合边界。这一步最难,需要梳理哪些对象必须在一个事务里修改。可以先用“如果一个操作需要同时改 A 和 B 且必须原子,那它们可能属于同一个聚合”这个粗糙标准做初筛,再逐个验证不变量。

第四步,把跨聚合协作改成领域事件。当聚合边界清晰之后,原来在一个大事务里做的多表更新,逐步拆成“改一个聚合 + 发布事件 + 订阅方异步处理”。

这条路径走下来通常需要几个月,而且中间状态会有点难看,但比一次性重写稳得多。我在一个中等规模系统上走过一遍,最大的感受是:不要试图一次改完,也不要为了“看起来像DDD”而改,每一步都应该有明确的业务收益,否则就是纯粹的形式主义。

6. 学DDD这段时间踩过的几个典型坑

概念学完到真正用对,中间隔着一段很长的弯路。下面这几个坑我基本都踩过,写出来供参考。

坑一:把DDD当成分层架构的改名运动。最容易发生的情况是:包结构改成四层,实体类加上几个业务方法,然后宣布迁移完成。但限界上下文没划、通用语言没统一、聚合边界没想清楚,结果只是把原来的大泥球换了个容器装着,复杂度一点没少。判断有没有真的用起来,我用的标准是:能不能指着代码说出每个聚合的不变量是什么,并且这些不变量确实由代码保证。说不出来,大概率还是贫血模型换了张皮。

坑二:聚合设计过大。一开始总觉得“这些数据都相关,放一起方便”,于是聚合越画越大。后果是并发冲突频发——两个人同时操作同一个聚合的不同部分,因为聚合是一个事务边界,只能串行。正确的做法是问自己:这两部分数据是否必须同时满足某个不变量?如果不是,就拆开。

坑三:通用语言只存在于会议纪要里。会上大家统一了叫法,散会后各写各的。这个坑的解法前面说过,就是让词汇进入代码评审、进入测试方法名,靠流程而不是靠自觉。

坑四:过度使用领域事件。领域事件很好用,但用多了会让调用链路变得难以追踪。我现在的原则是:同步能解决、且没有性能问题的,就不用事件。事件适合表达“已经发生的事实”和“跨聚合的最终一致”,不适合用来做流程编排。

坑五:把防腐层写成纯粹的字段搬运。防腐层的价值在于把外部模型翻译成本地模型,如果只是把外部 DTO 的字段一个个赋值到本地对象,那这层就白写了。真正的防腐层应该做语义转换:外部系统的“状态 3”要翻译成本地的“已确认”,外部的金额单位是分要转换成本地的元,外部的枚举值要映射成本地的枚举。这些转换规则集中在一处,外部系统变更时才不用满项目找。

坑症状判断方法修正方向
改名运动包名对了,模型还是贫血的说不出聚合不变量从值对象和不变量入手
聚合过大并发冲突多、加载慢一次事务改多张表按不变量拆分聚合
语言失效代码命名与业务词汇脱节业务方看不懂代码词表进评审
事件滥用链路难追踪、调试困难简单流程也走事件同步优先
防腐层空转只有字段赋值没有语义转换外部变更仍需大改补语义映射规则

最后分享一个我觉得挺有用的小技巧:给每个限界上下文写一份不超过一页的“边界说明”,内容包括这个上下文负责什么、不负责什么、对外暴露哪些能力、依赖哪些外部上下文、这个上下文里的核心词汇表。这份说明放在代码仓库里,新人进来第一件事就是读它。写了半年之后我发现,这份文档最大的价值不是给新人看的,而是逼着团队自己把边界想清楚——很多模糊地带在写说明的过程中就暴露出来了。

再补一句关于学习路径的体会。DDD 的书不容易读,概念之间的依赖关系是网状的,单独看“聚合”很难理解为什么要这么设计,得先理解限界上下文和一致性边界。我自己的顺序是:先搞明白通用语言和限界上下文(这两件事哪怕不写代码也能实践),再回头啃实体、值对象、聚合,最后才是领域事件和架构分层。反过来从战术模式入手,很容易学成一堆孤立的代码技巧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询