想象一下,你是一座城市的规划师。这座城市需要同时服务居民、企业和政府,而不同人群对“路”的理解截然不同——居民关心步行道是否绿树成荫,企业关心物流通道是否高效,政府则关心主干道能否疏散应急车流。如果你用一张“万能图纸”去设计所有道路,城市必然陷入混乱。聪明的规划师会划分功能区:住宅区、工业区、行政中心,每个区域有自己独立的道路标准和建筑规范,区域之间通过明确的接口衔接。 领域驱动设计(Domain-Driven Design,简称 DDD)正是软件世界里的“城市规划方法论”。它教会我们:复杂的业务系统必须拆分成若干个“限界上下文”(Bounded Context),每个上下文拥有自己独立的模型和语言,就像城市的每个功能区有自己的规划标准;而在每个上下文内部,我们需要通过“聚合根”(Aggregate Root)来管理一组紧密相关的对象,就像每个社区需要“居委会”来统一协调居民事务。

岗位关联

掌握 DDD 的限界上下文与聚合根,是架构师和资深后端工程师的必修课。它能帮你把混乱的业务需求转化为清晰、可维护、可演进的软件架构,是微服务拆分和云原生实践的核心理论基石。

目录
  1. 可视化演示
  2. 通用语言
  3. 限界上下文
  4. 上下文映射
  5. 聚合根设计
  6. 仓储与持久化
  7. 微服务与云原生
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

三步展示DDD战略与战术建模:限界上下文边界隔离、聚合根Order管理内部实体、领域事件实现聚合间异步解耦通信。

本节配有交互动效(进入视口后自动加载,约 3KB)。

限界上下文边界 订单:独立模型 库存:独立模型 支付:独立模型 Boundary = 边界 显式划定模型适用边界。

通用语言

城市规划师最头疼的事情是什么?不是画图纸,而是让所有参与方说同一种“话”。居民说“我要一条散步的道”,企业说“我要一条运货的路”,同一个物理空间被赋予了三四种叫法,施工队拿着图纸无所适从。

在 DDD 中,这个问题的答案叫“通用语言”(Ubiquitous Language)。它是业务专家、产品经理、开发者在同一限界上下文内共同约定的一套词汇表:每个术语的含义唯一且明确。比如在订单上下文里,“订单”就是“用户提交的、包含商品清单和支付状态的购买凭证”,谁都不能另造一个“单子”或"purchase"来混淆它。

通用语言不是挂在墙上的文档,而是从需求评审、代码注释、类名方法名到数据库字段名,全程使用的“活语言”。实践中我们通过“事件风暴”(Event Storming)工作坊,让业务人员和开发人员用黄色便签贴出领域事件、用蓝色便签贴出命令,在讨论中逐步收敛出语言共识。

回到城市规划的比喻:通用语言就是那座城市的“官方地名录”。每个地方只有一个标准名称,居民、企业、政府都用它,门牌号(代码)也按它来编。没有这本名录,城市就会出现“同路异名”的混乱;有了它,各方协作才有效率可言。

通用语言={术语∣含义唯一∧全员共用∧代码落地}\text{通用语言} = \{\text{术语} \mid \text{含义唯一} \land \text{全员共用} \land \text{代码落地}\}
公式解读:通用语言是一个集合,集合中的每个术语必须满足三个条件——含义唯一(不能有歧义)、全员共用(业务与技术都要用)、代码落地(最终体现在代码里)。
  1. 术语通用语言的基本单位,如“订单”、“客户”、“加购”等业务名词和动词。
    术语∈通用语言\text{术语} \in \text{通用语言}
  2. 含义唯一每个术语在一个限界上下文内只能有一种解释,杜绝“你说的订单和我说的订单不是一回事”。
    ∀x∈术语:含义(x)=唯一确定值\forall x \in \text{术语}: \text{含义}(x) = \text{唯一确定值}
  3. 全员共用从领域专家到前端开发,所有人使用同一套术语沟通,消灭“翻译层”。
    业务词汇=技术词汇\text{业务词汇} = \text{技术词汇}
  4. 代码落地术语最终映射为代码中的类名、方法名、变量名,语言共识贯穿始终。
    术语→类名/方法名/字段名\text{术语} \rightarrow \text{类名/方法名/字段名}

限界上下文

城市不会只有一张规划图。住宅区有住宅区的建筑规范,工业区有工业区的安全标准,它们可能对“道路宽度”有不同的规定——这是合理的,因为各自的目标不同。如果把工业区标准强加到住宅区,居民生活会受干扰;反之亦然。

DDD 中的“限界上下文”(Bounded Context)就是这个道理:它是领域模型的显式边界。在边界内部,模型是自洽的、完整的;不同上下文之间,同一个词可能代表完全不同的概念。经典的例子是“产品”:在“商品目录上下文”中,产品是一个有名称、价格、图片的描述性实体;在“库存上下文”中,产品是一堆有数量、库位、批次的可盘点物件。它们共享同一个“产品”ID,但模型完全不同。

为什么必须画这条边界?因为没有边界的模型会无限膨胀,最终谁也改不动。正如城市的功能区划让规划有章可循,限界上下文让团队可以独立演进自己的模型和代码库。微服务架构中,一个限界上下文通常对应一个或多个微服务,边界成为服务拆分的依据。

L上下文AeqL上下文BL_{\text{上下文A}} eq L_{\text{上下文B}}

回扣城市的比喻:限界上下文就像是城市中围墙明确的社区。社区内有自己的物业管理办法(模型),有统一的社区语言(通用语言),居民生活在一个自洽的微环境中。社区之间通过大门(接口)沟通,但谁也不干涉谁的内政(模型独立演进)。

限界上下文=⟨M,B,L⟩\text{限界上下文} = \langle M, B, L \rangle
公式解读:一个限界上下文由三要素组成——M(模型 Model),即该边界内的领域模型;B(边界 Boundary),即模型的作用范围;L(语言 Language),即边界内的通用语言。
  1. 模型 M边界内包含的实体、值对象、领域服务等建模元素。
    M={实体,值对象,领域服务}M = \{\text{实体}, \text{值对象}, \text{领域服务}\}
  2. 边界 B定义模型的作用范围,边界外的细节被隔离。
    B:模型仅在此范围内有效B: \text{模型仅在此范围内有效}
  3. 语言 L边界内的通用语言版本,与外部语言的映射需要显式转换。

上下文映射

城市的不同社区之间如何协作?有的社区是“友好邻里”,共同出资修一条跨社区道路;有的社区则竖起围栏,只留一道安检门;还有的社区干脆共用一座图书馆。三种方式对应了 DDD 中三种典型的“上下文映射”(Context Mapping)关系。

第一种是“合作关系”(Partnership):两个上下文团队共同制定演进计划,像邻居联合修路,一荣俱荣、一损俱损。第二种是“防腐层”(Anticorruption Layer):当一个上下文需要与另一个模型差异极大的上下文集成时,在客户端和服务端之间加一层翻译适配器,就像安检门对外来人进行“翻译和过滤”,保护本社区的模型不受外部污染。第三种是“共享内核”(Shared Kernel):两个团队共享一小部分模型代码,像共用图书馆,必须严格协调变更。

上下文映射的核心价值是“显式化”:把上下文之间的集成方式从隐性耦合变成明确的、有名字、有策略的关系。这样团队就知道——我们之间是合作关系,所以重大变更要一起开会;我们和它们是防腐层关系,所以我们要维护好那层适配代码,不允许外部模型渗透进来。

回到城市比喻:防腐层就像是两个语言不通的社区之间的“翻译站”。翻译站的存在不是为了改变本社区的语言,而是让外来者的诉求能被理解、被过滤,再以本社区的标准形式传入。没有翻译站,外来概念就会像野草一样长进本社区的街道,破坏原有的规划秩序。

上下文映射∈{合作关系,防腐层,共享内核,客户-供应商,分离方式}\text{上下文映射} \in \{\text{合作关系}, \text{防腐层}, \text{共享内核}, \text{客户-供应商}, \text{分离方式}\}
公式解读:上下文映射关系是一个枚举集合,常见的五种关系包括合作、防腐层、共享内核、客户-供应商(上游下游各有承诺)以及分离方式(彻底解耦)。实际项目中 80% 的集成场景用防腐层与合作关系即可覆盖。
  1. 合作关系两个上下文团队共同演进,共享计划、同步发布。
    Partner(A,B)⇒同步规划\text{Partner}(A, B) \Rightarrow \text{同步规划}
  2. 防腐层上下文 A 调用 B 时,增加转换层隔离 B 的模型。
    A→ACLB\text{A} \xrightarrow{\text{ACL}} \text{B}
  3. 共享内核A 和 B 共享部分模型源码,变更需联合评审。
    Kernel=ModelA∩ModelB\text{Kernel} = \text{Model}_A \cap \text{Model}_B

聚合根设计

城市管理中最棘手的问题是“谁说了算”。一个社区里有业主、租户、物业、访客……如果每个人都能直接改小区的规划,社区必然一片混乱。于是法律规定了“业主委员会”作为唯一决策入口:所有涉及小区的变更,必须经由业委会审批。

DDD 中的“聚合根”(Aggregate Root)就是这个“业委会”。一个聚合(Aggregate)是一组必须保持一致的领域对象集合,聚合根是这组对象的唯一入口和一致性守卫。例如“订单”聚合包含订单实体、订单项列表、配送地址值对象,对订单项的任何修改(加商品、改数量、删项)都必须经由聚合根“订单”来操作,不能绕过它直接操作订单项。

为什么要这样设计?因为聚合根定义了事务的边界:一个事务只跨越一个聚合——或者说,一个事务内只允许修改一个聚合的实例。这种约束极大简化了并发一致性问题的处理。外部对象想引用聚合内的对象,只允许持有聚合根的 ID 而不能持有内部对象的应用,从而保证聚合的封装性不被破坏。

回扣城市规划:聚合根就是社区“业委会”。你想在社区里加一盏路灯(修改聚合内对象)?必须向业委会提交申请(调用聚合根方法),业委会审批通过、公示(维护不变量)后才允许施工。对外界来说,它只知道“找业委会”这个入口,无须知道业委会内部如何协调各方。这样城市管理才有序,领域模型才能抵抗复杂性。

Aggregate=⟨R,{Ei},{Vj}⟩,外部仅通过 R.method() 访问\text{Aggregate} = \langle R, \{E_i\}, \{V_j\} \rangle,\quad \text{外部仅通过 } R.\text{method()} \text{ 访问}
公式解读:一个聚合由聚合根 R、若干实体 E、若干值对象 V 构成。外部世界只能通过聚合根 R 暴露的方法来访问或修改聚合内部,不能直接操作内部对象。
  1. 聚合根 R聚合的“门面”,唯一的对外访问入口,负责维护不变量。
    R∈Aggregate,R 持有内部对象引用\text{R} \in \text{Aggregate},\quad \text{R 持有内部对象引用}
  2. 实体 E聚合内拥有独立标识的业务对象,如订单项(有业务主键)。
    Ei=(idi,datai)E_i = (id_i, \text{data}_i)
  3. 值对象 V聚合内无独立标识、仅描述属性的对象,如配送地址。
    Vj=(attr1,attr2,… )V_j = (\text{attr}_1, \text{attr}_2, \dots)
  4. 不变量聚合内数据必须始终满足的一致条件,只在聚合根方法中校验和维护。
    Invariant(Aggregate)≡True\text{Invariant}(\text{Aggregate}) \equiv \text{True}

仓储与持久化

政府办公大楼里有一个“档案室”,所有社区的重要文件(房产证、户籍记录)都要在这里归档。社区办事不需要知道档案存放在哪个柜子、哪个抽屉,只需要向档案室提交申请——“把 3 号楼的入住记录调出来”。档案室自然会找到。

DDD 中的“仓储”(Repository)就是领域层的“档案室”。它为聚合根提供持久化访问的抽象接口:OrderRepository.findById(orderId) 返回一个订单聚合,save(order) 把订单聚合落库。关键是,仓储只服务于聚合根——它是聚合在内存中的集合形态,不允许绕过聚合根直接查询内部对象。

仓储的价值在于让领域模型保持“纯净”:模型只关心业务规则,不关心数据库是 MySQL 还是 MongoDB,不关心是 MyBatis 还是 JPA。数据的存储、查询、映射细节全部封装在仓储实现里。这样当数据库方案调整时(比如从单库拆分为分片数据库),领域层的业务逻辑纹丝不动。

回到城市档案馆的比喻:档案馆管理着全城的档案,但工作人员牢记一条规矩——涉及社区的事务,必须通过社区业委会(聚合根)来办,不能直接找业主个人(聚合内部对象)乱翻材料。档案存放在哪儿、用什么系统,居民根本不关心,他们只需要知道“去档案室查得到”。

Repository⊆{AggregateRoot},save(Agg)→Persistence,findById(id)→Agg\text{Repository} \subseteq \{\text{AggregateRoot}\},\quad\text{save}(\text{Agg}) \rightarrow \text{Persistence},\quad\text{findById}(id) \rightarrow \text{Agg}
公式解读:仓储的泛型参数被约束为聚合根类型,即仓储只服务于聚合根。它提供两个核心操作:保存聚合到持久化存储、根据 ID 恢复聚合。
  1. 约束泛型仓储接口的泛型上界是聚合根,从类型层面杜绝“仓储直接访问内部对象”。
    Repository<Agg extends AggregateRoot>\text{Repository<Agg extends AggregateRoot>}
  2. 方法流程save 方法接收聚合,由实现层(如 JPA 的 EntityManager)完成数据库写入。
    save(Agg)→INSERT/UPDATE\text{save}(\text{Agg}) \rightarrow \text{INSERT/UPDATE}
  3. 恢复流程findById 从存储中读取数据,重建聚合状态,返回完整的聚合对象。
    findById(id)→data→new Agg(data)\text{findById}(id) \rightarrow \text{data} \rightarrow \text{new Agg(data)}

微服务与云原生

城市发展到一定规模,单一的城市管理平面必然崩溃。现代城市选择走“多中心”路线:每个中心独立运营,中心之间通过高速交通网协作。这正是微服务与云原生架构的分工原则。

DDD 与微服务是天作之合:限界上下文为微服务划定了天然的边界——一个限界上下文对应一个微服务(或一组高内聚的微服务)。这样每个服务拥有独立的模型、独立的数据库、独立的部署管道,团队可以独立迭代。云原生环境进一步放大了这个优势:服务容器化后可以独立伸缩,订单服务的流量高峰不会殃及库存服务。

在云原生架构中,DDD 的聚合根与限界上下文变得更加“物理化”:聚合对应了数据一致性边界——跨聚合的事务被禁止,转而通过“最终一致性”(比如领域事件 + Saga 模式)来协调;限界上下文之间的集成变成了服务之间的 API 调用或消息队列事件。数据库也按上下文隔离——订单服务用订单库,库存服务用库存库,绝不共享表结构。

DBi∩DBj=∅(ieqj)\text{DB}_i \cap \text{DB}_j = \varnothing (i eq j)

回到城市规划:多中心城市格局就是云原生时代的架构图景。城市的每个“中心”(微服务)独立管理自己的区域(限界上下文)、拥有独立的资源(数据库),一个大片区停电不会拖垮整个城市网络(故障隔离)。而城市大脑(消息总线)负责在中心之间传递事件——“工业园区的货物已出库”——各方据此更新自己的台账,达成最终一致。

微服务边界=限界上下文边界,数据隔离=一上下文一库,一致性=最终一致性\text{微服务边界} = \text{限界上下文边界},\quad\text{数据隔离} = \text{一上下文一库},\quad\text{一致性} = \text{最终一致性}
公式解读:微服务划分的直接依据就是限界上下文;每个限界上下文独占自己的数据库;跨上下文的事务一致性让位于领域事件驱动的最终一致性。
  1. 服务边界一个限界上下文映射为一个可独立部署的微服务,上下文边界即服务边界。
    微服务i↔限界上下文i\text{微服务}_i \leftrightarrow \text{限界上下文}_i
  2. 数据隔离每个上下文独享数据库 Schema,杜绝“共享库”这种隐式耦合。
  3. 最终一致性跨上下文的数据同步通过领域事件 + 消息队列实现,允许短暂的不一致。
    Event→MQ消费→收敛一致\text{Event} \xrightarrow{\text{MQ}} \text{消费} \rightarrow \text{收敛一致}

AI 应用场景

应用场景一:电商订单系统的 DDD 改造

阶段:领域建模与架构设计

解决的问题:传统订单系统采用“贫血模型”,订单、商品、库存、支付耦合在一张巨型表中,每次需求变更都要改动核心表结构,迭代速度极慢。通过 DDD 划分出订单上下文、库存上下文、支付上下文三个限界上下文,通过聚合根“订单”统一管理订单项和金额计算,复杂的业务逻辑从 Service 层下沉到领域层。

具体案例:某电商平台对订单模块进行 DDD 重构。他们将订单、订单项、配送地址建模为一个聚合并以 Order 为聚合根,OrderService 不再处理计算逻辑,只做编排。重构后,新增“满减优惠”需求时只需在聚合根方法中增加一段逻辑,不再影响其他服务。订单上下文与库存上下文通过防腐层集成,库存接口变化时只改适配层。

应用场景二:银行核心系统的限界上下文划分

阶段:业务边界分析与服务拆分

解决的问题:银行核心系统包含存款、贷款、客户、风控等大量业务模块,若构建为单体应用会难以维护和到点发布。利用限界上下文识别出“存款账户上下文”“贷款合同上下文”“客户信息上下文”等六个边界清晰的模块,各自独立演进。

具体案例:某股份制银行的新核心系统采用 DDD 驱动微服务设计。他们以聚合根“存款账户”为中心管理账户、交易明细、利息规则;贷款模块通过领域事件“贷款审批通过”与账户模块异步联动,避免跨服务的分布式事务。云原生容器化部署下,下半年“开门红”活动带来的存款交易高峰可以独立扩容存款服务。

应用场景三:供应链系统的上下文映射实践

阶段:系统集成与数据同步

解决的问题:大型制造企业的 WMS(仓储管理系统)与 ERP(企业资源计划)模型差异巨大,直接集成会导致模型污染。使用防腐层隔离两个限界上下文,上层再以共享内核维护“物料主数据”的公共定义。

具体案例:某汽车制造企业的供应链平台中,WMS 关注物料在库位间的移动,ERP 关注物料的财务价值。两个系统通过防腐层 API 进行交互——ERP 调用 WMS 的“入库确认”接口时,防腐层将 WMS 的“托盘 SN”转换为 ERP 认识的“物料凭证号”。共享内核中的物料基础数据由双方联合维护,变更必须走变更评审。

Python 演示

python144 行
"""领域驱动设计核心概念演示:限界上下文与聚合根用一个轻量级的订单管理示例,展示 DDD 四大要素:通用语言、限界上下文、聚合根、仓储""" from abc import ABC, abstractmethodfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Optionalfrom enum import Enum  class OrderStatus(Enum):    CREATED = "已创建"    PAID = "已支付"    CANCELLED = "已取消"  @dataclass(frozen=True)class Price:    """值对象:金额,不可变"""    value: float    currency: str = "CNY"     def to_string(self) -> str:        return f"{self.value:.2f} {self.currency}"  @dataclassclass OrderItem:    """聚合内部实体:订单项"""    item_id: str    product_name: str    quantity: int    price: Price  class OrderAggregate:    """聚合根:订单"""     def __init__(self, order_id: str, customer_id: str):        self.order_id = order_id        self.customer_id = customer_id        self.status = OrderStatus.CREATED        self.items: List[OrderItem] = []     def add_item(self, item_id: str, name: str,                 qty: int, price: float) -> None:        """经由聚合根新增订单项,维护不变量"""        if self.status == OrderStatus.CANCELLED:            raise ValueError("已取消的订单不能新增商品")        if qty <= 0:            raise ValueError("商品数量必须为正数")        self.items.append(OrderItem(            item_id=item_id, product_name=name,            quantity=qty, price=Price(price)        ))     def total_price(self) -> float:        """聚合根统一计算订单总额"""        return sum(i.quantity * i.price.value for i in self.items)     def cancel(self) -> None:        """取消订单的唯二入口之一"""        if self.status == OrderStatus.PAID:            raise ValueError("已支付的订单需走退款流程")        self.status = OrderStatus.CANCELLED  class OrderRepository(ABC):    """仓储接口:仅服务于聚合根 OrderAggregate"""     @abstractmethod    def find_by_id(self, order_id: str) -> Optional[OrderAggregate]:        pass     @abstractmethod    def save(self, order: OrderAggregate) -> None:        pass  class InMemoryOrderRepository(OrderRepository):    """内存版仓储实现,模拟真实持久化"""     def __init__(self) -> None:        self._store: Dict[str, OrderAggregate] = {}     def find_by_id(self, order_id: str) -> Optional[OrderAggregate]:        return self._store.get(order_id)     def save(self, order: OrderAggregate) -> None:        self._store[order.order_id] = order  class OrderApplicationService:    """应用服务:编排用例,不承载业务规则"""     def __init__(self, repo: OrderRepository):        self.repo = repo     def create_order(self, order_id: str, customer_id: str) -> OrderAggregate:        order = OrderAggregate(order_id, customer_id)        self.repo.save(order)        return order     def add_item(self, order_id: str, item_id: str, name: str,                 qty: int, price: float) -> None:        order = self.repo.find_by_id(order_id)        if order is None:            raise ValueError(f"订单 {order_id} 不存在")        order.add_item(item_id, name, qty, price)        self.repo.save(order)     def cancel_order(self, order_id: str) -> None:        order = self.repo.find_by_id(order_id)        if order is None:            raise ValueError(f"订单 {order_id} 不存在")        order.cancel()        self.repo.save(order)  def main() -> None:    repo = InMemoryOrderRepository()    order_service = OrderApplicationService(repo)     order = order_service.create_order("ORDER-001", "CUST-2024")    order_service.add_item("ORDER-001", "ITEM-1", "机械键盘", 1, 399.0)    order_service.add_item("ORDER-001", "ITEM-2", "显示器", 2, 1299.0)     fetched = repo.find_by_id("ORDER-001")    print(f"订单号: {fetched.order_id}")    print(f"状态: {fetched.status.value}")    print(f"商品数: {len(fetched.items)}")    print(f"订单总额: {fetched.total_price():.2f} 元")     order_service.cancel_order("ORDER-001")    print(f"取消后状态: {repo.find_by_id('ORDER-001').status.value}")     # 违反聚合根约束的示例,将被拦截    # order_service.add_item("ORDER-001", "ITEM-3", "鼠标", 1, 99.0)  if __name__ == "__main__":    main()
目的:演示 DDD 中聚合根如何封装业务规则、仓储如何隔离持久化细节,以及应用服务如何只做编排不承载业务逻辑。
  1. class OrderAggregate聚合根类,对外提供业务方法的唯一入口,内部维护不变量
  2. def add_item(...)聚合根方法:先校验状态和数量,再修改内部状态
  3. OrderStatus.CANCELLED枚举定义的业务状态,是通用语言在代码中的落地
  4. class OrderRepository(ABC)抽象仓储接口,泛型上界隐含为聚合根
  5. class InMemoryOrderRepository(OrderRepository)仓储实现,内部用字典模拟数据库
  6. class OrderApplicationService应用服务层,只做“找仓储 → 调聚合 → 存仓储”的编排
  7. order.add_item(...)经由聚合根修改数据,杜绝绕过入口直接操作 items 列表
  8. order_service.cancel_order(...)应用服务校验订单存在后,委托聚合根执行业务规则
  9. self._store: Dict[str, OrderAggregate]仓储内部存储结构,可替换为 MyBatis/JPA 实现

常见误区

误区以为引入 DDD 就是每天开站立会、用敏捷看板、写用户故事,甚至觉得 DDD 是“敏捷开发的升级版”。

正解DDD 是“软件建模与设计方法”,它关心如何把业务知识转化为领域模型、如何划分模型边界,而不是项目管理流程。敏捷看板是流程管理工具;限界上下文、聚合根是设计产物。就像城市规划方法论教你如何规划街区,而不是教你如何给施工队排工期。敏捷与 DDD 可以互补,但绝不是一个东西。

为何常见:因为 DDD 常常在敏捷项目中推广,且事件风暴工作坊看起来很像敏捷会议,初学者容易混淆“建模方法”与“管理方法论”。

误区为了“保证一致性”,把所有有关联的实体放进同一个聚合,一个订单聚合把商品、库存、支付、发票全塞进来。

正解聚合太大必然导致并发冲突加剧、事务性能下降。每次订单变更都要锁定库存,这种跨聚合操作在微服务架构下根本行不通。正确的策略是“小聚合”:每个聚合只维护一组必须同时满足的强不变量,跨聚合的一致性靠领域事件达成最终一致。就像社区业委会只管社区内部的事,市政道路由城管局管,各自管好自己的一亩三分地。

为何常见:人们天然倾向于“把所有好东西放在一个篮子里”,以为这样最安全,忽略了性能、并发和事务范围的代价。

误区把聚合内的 List\<OrderItem\> 用 public getter 暴露出来,外部代码直接调用 `order.items.add(...)`。

正解这个行为会击穿聚合的封装,导致业务规则被绕过——比如跳过“已取消订单不能加购”的校验。正确的做法是聚合根不暴露内部集合的可写引用,外部只能调用聚合根提供的方法(如 addItem)。如果确实需要遍历,返回不可变副本或 stream。类比:业委会不能让业主直接改规划图,所有变更必须经业委会审批。

为何常见:开发习惯使然——用 Lombok 的 @Data 无脑生成 getter/setter,忽略了对象内聚与封装原则,让“结构”变成了“裸数据”。

误区写一个 Repository 接口,里面粗暴定义 `insert/update/delete/selectByCondition`,本质仍是数据访问对象。

正解DDD 的仓储是以“聚合”为单位操作领域对象的集合仓库,接口语义是业务性的:findById(orderId) 返回完整聚合,save(order) 保存聚合变更。它不提供 update 之类的数据操作,也不允许按任意条件查询(那会破坏聚合封装)。一个订单仓储不会让你做“按商品名查订单项”这种暴露内幕的查询——要查也要走专门的查询通道(CQRS 的读模型)。

为何常见:因为多数人是先会了 MyBatis/MyBatis-Plus 的 DAO 再学 DDD,习惯性地把 Repository 接口当 DAO 模板套用,没意识到两者差异在“聚合的概念边界”。

误区把代码里的一个 package(如 `com.company.order.model`)当作限界上下文,认为“分了包就叫分了上下文”。

正解限界上下文是一个“业务边界概念”,体现在代码上通常是独立的部署单元(微服务)、独立的数据库 Schema、独立的团队归属。同一个 package 里出现订单模型和库存模型,并不代表它们属于不同上下文。判断标准是:一个上下文拥有独立的模型演进周期和部署生命周期,能“各自独立修改而不必同步发布”。否则只是“逻辑分包”,不是 DDD 意义的上下文。

为何常见:代码分包是肉眼可见的、容易定义的;而业务边界的划分需要理解业务组织结构、发布节奏和变化频率,门槛高,于是初学者就拿“分 package”这种表面结构来麻醉自己。

面试题

Q1. 以下哪一个是限界上下文(Bounded Context)最准确的描述?

单选题 · mid

  • A.一个代码模块的命名空间范围
  • B.领域模型的显式业务边界,内部模型自洽且独立演进← 正确答案
  • C.一个微服务的全部代码仓库
  • D.数据库中一组表的集合

答案:B。限界上下文是领域模型的显式边界,边界内模型语义自洽、可独立演进。A 的命名空间只是实现层面的一种落地方式;C 可能一个服务包含多个上下文;D 的表集合是数据视角,无法表达业务模型的完整性。

Q2. 聚合根(Aggregate Root)的核心职责是什么?

单选题 · easy

  • A.提供对聚合内所有实体的全局查询
  • B.作为聚合内实体的一致性守卫和唯一对外访问入口← 正确答案
  • C.负责聚合与外部的所有网络通信
  • D.管理数据库连接池

答案:B。聚合根是聚合内所有对象的一致性和封装性守卫,外部只能通过聚合根暴露的方法访问聚合内部。它不负责全局查询(那是仓储的职责)、不负责网络通信(那是应用服务/基础设施的职责)。

Q3. 关于“通用语言”(Ubiquitous Language),以下说法正确的是?

多选题 · mid

  • A.是业务人员和开发人员约定的一套统一术语← 正确答案
  • B.这套语言只存在于需求文档中
  • C.术语最终应落实为代码中的类名和方法名← 正确答案
  • D.不同限界上下文内允许对同一术语有不同的定义← 正确答案

答案:ACD。通用语言是从需求到代码全程使用的词汇表,需落实在代码中,A、C 正确。D 正确——同一术语在不同限界上下文可以含义不同(如“产品”在商品上下文和库存上下文语义不同)。B 错误,通用语言不能只停留在文档,它必须活跃于沟通和代码中。

Q4. 以下哪种方式违反了聚合根的设计约束?

单选题 · hard

  • A.外部代码通过聚合根的方法新增一个订单项
  • B.外部代码直接获取聚合内 List&lt;OrderItem&gt; 并调用 add 方法← 正确答案
  • C.应用服务调用订单聚合根的 cancel() 方法
  • D.仓储通过 findById 将完整的订单聚合从数据库恢复

答案:B。B 直接操作聚合内部对象集合,击穿了聚合的封装,是典型违规。A、C 均通过聚合根暴露的方法访问,符合规则。D 是仓储的正常职责——恢复聚合的对象图,但这并不破坏封装,因为恢复完成后对外再次暴露的是聚合根。

Q5. “防腐层”(Anticorruption Layer)的主要作用是?

单选题 · easy

  • A.防止数据库被恶意攻击
  • B.在两个模型差异较大的上下文之间做翻译和隔离← 正确答案
  • C.对微服务接口做限流熔断
  • D.保护前端页面不被篡改

答案:B。防腐层用于保护一个限界上下文不被外部上下文的模型污染,通过适配器将外部模型翻译为本上下文的模型。它不是安全防护、不是流量治理、也不是前端安全。

Q6. 关于聚合根与事务边界,说法正确的是?

多选题 · mid

  • A.一个事务内建议只修改一个聚合实例← 正确答案
  • B.跨聚合的数据一致性应优先考虑最终一致性← 正确答案
  • C.聚合根越大,事务性能越好
  • D.在微服务架构中,跨聚合事务必须拆分为领域事件驱动的最终一致← 正确答案

答案:ABD。DDD 主张一个事务只修改一个聚合实例,跨聚合一致性优先用最终一致性。A、B、D 正确。C 错误——聚合越大,锁冲突和事务范围越大,性能反而越差。

Q7. 在 DDD 分层架构中,一个订单金额的计算逻辑应该放在哪一层?

单选题 · mid

  • A.应用服务(Application Service)
  • B.领域层的聚合根(Aggregate Root)← 正确答案
  • C.基础设施层的仓储实现
  • D.控制器(Controller)

答案:B。金额计算属于核心业务规则(领域逻辑),应放在聚合根或领域服务中。应用服务只做编排,仓储只做持久化,控制器只接收 HTTP 请求并返回响应。

Q8. 微服务拆分时,最合理的依据是?

单选题 · mid

  • A.代码行数均匀划分
  • B.按限界上下文边界划分← 正确答案
  • C.按数据库表归属划分
  • D.按团队人数平均分配

答案:B。DDD 明确建议“一个限界上下文对应一个微服务”,因为上下文的模型自洽性和演进独立性正是微服务所需的内聚与解耦。代码行数、表数量和团队人数都不直接反映业务模型边界。

Q9. 以下哪些属于 DDD 中“上下文映射”的关系类型?

多选题 · hard

  • A.合作关系(Partnership)← 正确答案
  • B.防腐层(ACL)← 正确答案
  • C.共享内核(Shared Kernel)← 正确答案
  • D.主从复制(Master-Slave Replication)

答案:ABC。DDD 的上下文映射关系包含合作、防腐层、共享内核、客户-供应商、分离方式等。主从复制是数据库层面的数据同步技术,不在此列。

Q10. 通用语言与限界上下文的关系是?

单选题 · easy

  • A.每个限界上下文内各有一套通用语言← 正确答案
  • B.全系统只有一套通用语言
  • C.通用语言与限界上下文无关
  • D.通用语言只用于数据库命名

答案:A。每个限界上下文拥有自己专属的通用语言版本,其术语含义只在边界内有意义。同一个词在不同上下文可以有不同的语义,这正是 B(唯一一套全球通用)的反例。