Skip to content

限界上下文 ​

核心问题:如何划分限界上下文?上下文之间如何映射?如何避免"大泥球"?

1. 什么是限界上下文 ​

限界上下文(Bounded Context)是 DDD 中的核心战略模式,它定义了一个模型的适用边界。在边界内,所有术语和规则保持一致。

txt
┌─────────── 电商系统 ────────────┐
│                                │
│  ┌─────────────┐ ┌───────────┐ │
│  │  交易上下文  │  │ 用户上下文 │ │
│  │             │ │           │ │
│  │ 买家、卖家   │ │ 注册用户    │ │
│  │ 订单、支付   │ │ 认证信息    │ │
│  │ 物流地址     │ │ 偏好设置   │ │
│  └─────────────┘ └───────────┘ │
│                                │
│  ┌─────────────┐ ┌───────────┐ │
│  │  库存上下文   │ │ 搜索上下文 │ │
│  │             │ │           │ │
│  │ SKU、库存    │ │ 商品索引   │ │
│  │ 仓库、批次   │  │ 搜索词    │ │
│  └─────────────┘ └───────────┘ │
└────────────────────────────────┘

2. 如何划分限界上下文 ​

java
// 同一个词在不同上下文中的含义不同
// "商品" 在不同上下文中的含义:

// 商品上下文:完整的商品信息
class Product {
    Long id;
    String name;
    String description;
    List<String> images;
    BigDecimal price;
    Category category;
}

// 库存上下文:商品的库存信息
class InventoryItem {
    String sku;           // 用 SKU 而非商品 ID
    int quantity;
    String warehouseCode;
}

// 搜索上下文:商品的搜索索引
class ProductIndex {
    String productId;
    String title;
    String[] tags;
    double score;         // 搜索相关性分数
}

// 订单上下文:订单中的商品快照
class OrderProduct {
    String productId;
    String productName;   // 下单时的名称快照
    BigDecimal price;     // 下单时的价格快照
    int quantity;
}

四个模型的字段不同,但字段差异本身不是划分上下文的依据。真正决定边界的是业务规则和一致性责任。商品改价后,商品主数据、订单历史价格、搜索索引不能共用同一个领域模型:商品上下文关心当前价格,订单上下文必须保留下单那一刻的价格快照,搜索上下文只维护索引所需的最小字段。库存按 SKU、仓库维度管理,订单保存快照是为了保留下单时的业务事实。这些都是职责与规则的结果,不是按字段裁剪实体。

3. 上下文映射模式 ​

模式说明适用场景
共享内核(Shared Kernel)两个上下文共享部分模型紧密耦合的上下文
客户-供应商(Customer-Supplier)下游依赖上游,上游交付需纳入下游需求存在供需依赖的上下游
防腐层(ACL)通过适配器隔离外部模型集成遗留系统或第三方
开放主机服务(OHS)提供标准化协议供多方使用平台型服务
各行其道(Separate Ways)两个上下文完全独立无业务关联

实际选择模式先看上下游关系。交易上下文依赖商品上下文获取商品主数据,通过明确契约调用,并在下单时保存必要的商品与价格快照。交易上下文对接第三方支付时,通过防腐层把支付平台的状态与协议映射为内部支付模型,避免外部模型渗入领域核心。如果上游不接受下游的模型需求,下游可能采用符合者(Conformist)策略迁就上游,也可能通过防腐层隔离;符合者与防腐层可在同一集成关系中结合使用,具体取决于双方协作意愿与业务约束。

java
// 防腐层(ACL)示例:隔离第三方支付系统
// 外部模型
class AlipayResponse {
    String trade_no;
    String total_amount;
    String trade_status;
}

// 防腐层:将外部模型转换为领域模型
class AlipayAdapter implements PaymentGateway {
    private final AlipayClient alipayClient;
    
    @Override
    public PaymentResult charge(PaymentCommand command) {
        AlipayResponse response = alipayClient.pay(/* ... */);
        // 转换为领域模型
        return new PaymentResult(
            mapStatus(response.trade_status),  // 将支付宝状态映射为内部状态
            new Money(new BigDecimal(response.total_amount), Currency.CNY)
        );
    }
    
    private PaymentStatus mapStatus(String alipayStatus) {
        // 将支付宝状态映射为内部支付状态。
        // 映射规则取决于支付平台契约与内部状态机:
        // 未知状态不应默认为「处理中」,需区分协议变更与解析错误。
        // 此处省略具体映射,仅示意防腐层的模型转换职责。
        return PaymentStatusMapper.from(alipayStatus);
    }
}

4. 微服务与限界上下文的映射 ​

限界上下文约束的是领域模型与统一语言的适用范围,是逻辑边界;微服务是部署单元,是物理边界。两者没有固定的一一映射。

情况处理
上下文需要独立部署、独立扩缩容或团队自治映射为独立微服务
规模较小、部署独立性不足可在模块化单体中保留多个限界上下文
一个上下文拆成多个服务应有明确的运行时或组织需求
多个上下文共处一个服务必须保留清晰的模型、模块和依赖边界
java
// 一个限界上下文独立部署时
// service-order     → 交易上下文
// service-inventory → 库存上下文

// 模块化单体:多个上下文共处一个部署单元
// 交易、用户、库存、搜索各自保持模型与模块边界
// 只有当它们共享领域模型、相互依赖并需要协调发布时,
// 才构成值得警惕的耦合(大泥球),而非仅因同处一个服务

核心原则:限界上下文不是按技术层划分,而是按业务语义划分。同一个"用户"在不同上下文中的含义和属性可能完全不同,这就是为什么要用不同的模型来表示。

Released under the MIT License.