Skip to content

外部依赖声明落地模式 ​

本文档介绍 Pragmatic DDD 中外部依赖声明(@ExternalDependency / IDependency / DependencyType)的落地方式:领域层如何声明「依赖了哪个外部目标」、防腐适配器(ACL)如何实现、应用层如何消费,以及约束与反模式。前置阅读:核心:外部依赖声明。

1. 本质与定位 ​

外部依赖声明(io.pragmatic.ddd.dependency)是领域层的元数据声明:描述「本聚合依赖了哪个外部目标(聚合 / 系统)」,而不负责真正的远程调用。它与 @DomainService 对称——一个声明「我需要什么能力」,一个声明「我依赖什么外部聚合 / 系统」(见 领域服务落地模式)。

真正的调用、协议转换、异常分类由防腐适配器(ACL)在基础设施层实现,二者职责不可混用:

维度外部依赖声明(领域层)防腐适配器(基础设施层)
形态@ExternalDependency + IDependency 接口@Component 实现领域端口
职责声明「依赖了什么」封装「怎么调用外部」
是否远程调用否是

声明即血缘:即便运行期不强制校验,声明仍是架构可视化 / 依赖分析的唯一来源,遵循「先定义后实现」——即使目标聚合 / 系统尚未就绪,也先声明端口,适配器打桩。

2. 命名与包结构 ​

2.1 包结构 ​

text
domain/order/
└── dependency/   外部依赖端口声明(IUserDependency 等,继承 IDependency 并标 @ExternalDependency)

infrastructure/order/
└── dependency/   防腐适配器实现(UserGatewayAdapter 等,@Component 实现领域端口)

2.2 命名规范 ​

元素格式示例
依赖声明接口I{目标}Dependency(继承 IDependency,标 @ExternalDependency)IUserDependency
外部系统依赖接口I{目标系统}ClientDependency(type = EXTERNAL_SYSTEM)IPaymentGatewayDependency
targetName小写下划线 / 连字符,全局唯一,与适配器指向的外部目标一致user、payment-gateway
防腐适配器实现类{目标}GatewayAdapter / {目标}ClientAdapter(@Component,基础设施层)UserGatewayAdapter

接口名必须体现业务意图;接口名以 I 开头,供架构扫描统一识别。

3. 数据 / 职责承载 ​

承载不承载
声明元数据(targetName / type / description)远程调用、协议转换、异常分类(归 ACL)
端口接口方法签名(声明「能取什么 / 能做什么」)运行期可用性保证(声明不强制校验)
业务意图描述(description 解释依赖目的)外部契约对象(对方 Request / Response / DTO / SDK 对象)进入领域模型

端口由应用层服务 / 事件订阅者通过构造注入消费;领域模型不持有远程状态、不承载外部契约对象(对方 Request / Response / DTO / SDK 对象)。

外部返回值须先经 ACL 的「响应转换」产出本上下文的领域值(基本类型 / String / 值对象 / 枚举),之后方可作为聚合字段持久化——持久化的是转换后的领域值,不是外部响应本身(见 core/dependency.md §3.4)。

4. 落地方式(核心) ​

以下以 Order 聚合为例演示示例写法。落地时请把 Order / User / PointsService 等替换为你自己的聚合名与外部目标——命名规范(§2.2)与反模式(§6)均为聚合无关规则,可原样套用。

4.1 领域层声明端口 ​

在 domain/order/dependency/ 下定义接口,继承 IDependency 并标注 @ExternalDependency。targetName 必填且全局唯一;type 默认 AGGREGATE,跨系统边界显式声明 EXTERNAL_SYSTEM:

java
package io.pragmatic.ddd.example.order.domain.order.dependency;

@ExternalDependency(
        targetName = "User",
        type = DependencyType.AGGREGATE,
        description = "用户聚合:提供用户等级以决定订单金额折扣,并提供用户是否生效且具备下单资格的判定"
)
public interface IUserDependency extends IDependency {

    int getUserLevel(String userId);

    String getUserMobile(String userId);

    boolean isUserQualified(String userId);
}

4.2 防腐适配器实现(基础设施层) ​

在 infrastructure/dependency/order/ 用 @Component 实现领域端口,封装「怎么调用外部」。目标未就绪时先打桩,运行期不强制校验:

java
package io.pragmatic.ddd.example.order.infrastructure.dependency.order;

@Component
public class UserGatewayAdapter implements IUserDependency {

    @Override
    public int getUserLevel(String userId) {
        return 0;                       // 打桩:真实场景查询用户聚合 / 用户服务
    }

    @Override
    public String getUserMobile(String userId) {
        return "13800000000";           // 打桩
    }

    @Override
    public boolean isUserQualified(String userId) {
        return true;                    // 打桩
    }
}

4.3 应用层消费端口 ​

应用层服务构造注入端口,领域模型不感知远程调用。下例用用户等级计算折扣:

java
@Service
public class OrderTotalAmountCalculator implements IOrderTotalAmountCalculator {

    private final IUserDependency userDependency;

    public OrderTotalAmountCalculator(IUserDependency userDependency) {
        this.userDependency = userDependency;
    }

    @Override
    public Money calculate(List<OrderItem> items, Order entity) {
        int level = userDependency.getUserLevel(String.valueOf(entity.getCustomer().getCustomerId()));
        BigDecimal rate = discountRateOf(level);          // 等级→折扣率映射内聚于此
        return items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(Money.ZERO, Money::add)
                .multiply(rate);
    }
}

4.4 外部系统写依赖 + 幂等 ​

对写外部(如发放积分),声明端口后,适配器继承 AbstractIdempotentWriteGateway,以业务幂等键(bizId)保证重复事件不重复发放:

java
@ExternalDependency(targetName = "PointsService", type = DependencyType.EXTERNAL_SYSTEM,
        description = "积分服务:按业务幂等键为指定用户增加积分")
public interface IUserPointsDependency extends IDependency {
    void increasePoints(IncreasePointsCommand command);   // command 携带 bizId 幂等键
}
java
@Component
public class UserPointsDependencyAdapter
        extends AbstractIdempotentWriteGateway<IncreasePointsCommand, Void, PointsRequest, PointsResponse, String>
        implements IUserPointsDependency {

    @Override
    public void increasePoints(IncreasePointsCommand command) {
        write(command);                                    // 先查后写,bizId 命中则短路
    }

    @Override
    protected String uniqueKey(IncreasePointsCommand param) {
        return param.bizId();
    }
    // toExternalRequest / doWrite / queryByKey 等钩子封装真实调用与幂等查询
}

外部返回值转换后可入领域:上面 increasePoints 是 void;若外部调用会返回标识符(如库存预占返回预占单号、支付返回流水号、实名认证返回认证流水号),同样由端口以领域类型返回。ACL 在 toDomainResult 完成响应转换后,该标识可作为聚合字段随本聚合事务落库:

java
@ExternalDependency(targetName = "inventory", type = DependencyType.EXTERNAL_SYSTEM,
        description = "库存系统:按订单预占库存并返回预占单号")
public interface IInventoryDependency extends IDependency {
    /** 预占库存,返回预占单号(已由 ACL 转为领域值,可直接进聚合并持久化)。 */
    String reserve(String sku, int qty);
    /** 释放预占(逆向操作,须幂等)。 */
    void release(String reserveNo);
}

// 聚合仅将其当作普通字段持有,不感知其来自远程
public class Order extends AggregateRoot<Long> {
    private String inventoryReservationNo;   // 预占单号:后续核销 / 释放 / 对账的依据
}

单值标识符直接用 String / 基本类型承载即可,无需包装为值对象;仅当外部产物是复合结构(多字段组合)时才用值对象表达。

4.5 装配 ​

端口与适配器均交由 Spring 管理:适配器 @Component 后即可被应用层按类型注入,无需额外 @Bean;@ExternalDependency 仅作元数据,不参与运行期装配。

5. 关键机制与避坑 ​

  • targetName 必填、全局唯一:是依赖关系的唯一标识,须与防腐适配器指向的外部目标一致(见 core/dependency.md §2.1/§6)。声明叫 inventory、适配器指向 stock,架构血缘 / 可视化无法关联。
  • 运行期不强制校验:声明未实现 / 不可用也不会报错,不能当作「可用性保证」;端口注入缺失由容器负责(见 core/dependency.md §3.3)。
  • 仅标记「依赖了什么」:远程调用、协议转换、异常分类归 ACL,禁止塞进依赖接口(见 core/dependency.md §3.3)。
  • 接口以 I 开头、继承 IDependency:供架构扫描统一识别。
  • type 默认 AGGREGATE:跨系统边界显式声明 EXTERNAL_SYSTEM(见 core/dependency.md §2.3)。
  • 端口消费在应用层:端口由应用层服务 / 事件订阅者构造注入消费;外部调用经事件订阅者 / 应用服务触发(见 事件订阅领域服务落地模式),不进本聚合数据库事务,避免外部不可用导致本地事务回滚风暴。
  • 外部派生值可入领域:被禁止进入领域模型的是外部契约对象(对方 Response / DTO),不是数据本身。外部返回值经 ACL 转换为领域值后,可作为聚合字段随本聚合事务落库(见 core/dependency.md §3.4)。

⚠️ targetName 不一致是高频坑:声明与实现用同一全局唯一 targetName,否则依赖可视化 / 架构分析无法把声明与实现关联起来。

6. 常见反模式 ​

反模式问题正确做法
领域层直接写 HTTP / RPC 调用违反分层与依赖倒置,领域模型耦合远程细节定义 I{目标}Dependency 端口 + 基础设施 ACL 适配器
把外部契约对象(对方 Response / DTO)直接作为聚合字段外部契约污染领域,对方改版本即破坏本聚合ACL toDomainResult 转为领域值后再入聚合(见 core/dependency.md §3.4)
targetName 命名与适配器目标不一致架构血缘 / 可视化关联失败声明与实现用同一全局唯一 targetName
把 @ExternalDependency 当运行时可用性保证实则不校验,误以为缺依赖会被拦截端口注入缺失由容器负责,声明只管血缘
把调用行为 / 异常分类写进依赖接口声明与调用职责混用接口只定义方法签名;实现侧封装转换与 AclExceptions
只写 ACL 适配器不声明失去架构血缘与依赖分析先定义 I{目标}Dependency 端口,再实现适配器
一个接口塞多个目标 / 多个外部系统血缘无法细分、难以替换一目标一端口(一接口一职责)
写外部不幂等重复事件导致积分重复发放等继承 AbstractIdempotentWriteGateway,以 bizId 先查后写

7. 下一步 ​