DIP 说高层策略不应依赖低层细节。Ports and Adapters 把这个方向放到系统边界上:应用层面向端口,数据库、消息、支付、外部服务都是适配器。DDD 战术设计再补一个约束:领域不变量不应丢给适配器或应用脚本去补。

Repository 是端口,不是 GoF 名称复用

labs/E06 的 OrderRepository 只有 save 和 find,InMemoryOrderRepository 是内存适配器。PlaceOrder 应用服务依赖接口,测试把订单 o-1 放进去,再从 repository 找回同一个对象。应用层不知道底层是 Map,也不需要知道未来会不会换成数据库。

1
2
Scenario.PlaceOrder useCase = new Scenario.PlaceOrder(
repository, new Scenario.OrderFactory(), cents -> cents <= 1_000);

Repository 不是 GoF 23 种模式之一。Factory 在这里也不是 Abstract Factory 或 Factory Method,而是 DDD 战术里的创建职责:OrderFactory 保证订单创建时已经有第一条订单行。名称相似不能替代意图和调用者边界。

不变量留在领域对象里

Line 拒绝空 SKU 和非正金额;Order.add 在订单批准后拒绝继续添加;Order.approve 要求订单非空,并调用 CreditPolicy 判断授信。仓储只保存和查找,不修正负金额,也不吞掉授信失败。

应用服务负责编排:创建订单、调用领域方法、保存结果。CreditPolicy 在本章只是一个布尔策略端口,用来模拟“授信是否通过”这条可替换规则;它可以演进成领域服务,但当前代码没有证明它必然就是领域服务。聚合和值对象保护内部一致性,Repository port 隔离外部存储。把这些职责都叫“抽象层”会模糊真正的变化方向。

flowchart LR
    UseCase[PlaceOrder 应用服务]
    Factory[OrderFactory]
    Policy[CreditPolicy 策略端口]
    Repo[OrderRepository 端口]
    Memory[InMemoryOrderRepository 适配器]
    Order[Order 聚合]

    UseCase -- 运行调用 --> Factory
    UseCase -- 运行调用 --> Policy
    UseCase -- 运行调用 --> Repo
    Factory -- 创建 --> Order
    Memory -- 实现端口 --> Repo

Ports and Adapters 保护的是系统边界,DDD 战术对象保护的是领域语义。端口可替换,不代表不变量可以离开领域模型。图里把运行调用和源码实现边分开:应用服务调用端口,内存适配器实现端口。

验证与练习

运行 ./mvnw -B -ntp -pl labs/E06 -am test;PortsAndDomainTest 通过 3 个测试。原始日志在 examples/design-patterns/evidence/E06/targeted.stdout.txt。真实数据库适配器、事务边界、领域事件发布 NOT_RUN。

  1. 新增 RecordingOrderRepository 测试替身,复用同一 PlaceOrder 测试,证明应用服务只依赖端口。
  2. 增加“总金额超过 1000 需要人工审核”。先写失败断言,再判断它属于 CreditPolicy、Order 还是应用服务。

参考资料:

上一节:E05 依赖注入容器、动态代理与 AOP;下一节:E07 真实库里能看到哪些模式。