Anti-Corruption Layer(防腐层)
解决什么问题:当本 BC 必须接入外部系统(遗留系统 / 第三方 API / 别的 BC)时,怎么保护本 BC 的模型不被污染—— 不加 ACL = 外部的字段、表结构、错误码渗进本 BC = 一次接入、永久债。 ACL = 一层"翻译 + 隔离",把外部的脏数据转成本 BC 的领域对象。
它是什么
防腐层(ACL, Anti-Corruption Layer) 是 DDD 战略模式之一——一个 BC 与外部系统交互时,在边界上做的翻译 / 适配 / 隔离层。
外部系统可能是:
- 遗留系统(Legacy System)
- 第三方 API(支付、物流、风控)
- 上游 BC(上游改了字段,本 BC 不应被影响)
- 外部数据源(数据库、文件、消息)
ACL 的本质:把外部世界的"语言"翻译成本 BC 的"通用语言",让本 BC 完全感知不到外部的存在。
本 BC 内部代码:
Order placeOrder(CustomerId cid, Cart cart) { ... } ← 干净的领域语言
ACL 翻译层:
class LegacyOrderAdapter {
private LegacySOAPClient legacy; ← 脏的外部接口
Order toDomainOrder(LegacyOrderDTO dto) {
return Order.reconstitute(
new OrderId(dto.getOrderNo()),
new CustomerId(dto.getCustNo()),
dto.getItems().stream().map(this::toItem).toList()
);
}
}Vernon Ch 3: "Use the Anticorruption Layer pattern to create a translation layer between your model and another system... This layer translates in both directions as necessary."
V2 视角(V2.7 B3 段 5 "团队可拥有" + B4 段 4 "抽象边界"):ACL 是 L1 战术层对 L4 / L5 演化的关键防线——外部再变,本 BC 的领域代码一行都不用改。
关键属性
| 属性 | 值/含义 |
|---|---|
| 位置 | BC 边界,外部 → 本 BC 的方向 |
| 职责 | 翻译(格式)、适配(协议)、隔离(错误) |
| 方向 | 双向(入向翻译 + 出向翻译) |
| 不可绕过 | 本 BC 任何代码不直接调外部 API |
| 可降级 | 外部挂了 ACL 兜底(缓存 / 默认值) |
| 可测试 | 用 Adapter 接口隔离,方便 mock |
| 演进性 | 外部升级 → 只改 ACL 适配器,不影响领域层 |
| 反模式 | "图省事直接 MapStruct 一下" = 不是 ACL,是污染 |
怎么用
场景 1:接入遗留 SOAP 系统
本 BC(Order)
│
↓ 调 OrderService(干净接口)
┌───────┐
│ ACL │ ← 翻译层
└───┬───┘
│ 调 legacy.placeOrder(xml)
┌───↓──────────┐
│ Legacy SOAP │ ← 老 ERP 系统
└──────────────┘ACL 实现:
// 1. 本 BC 期望的接口(端口)
public interface ExternalOrderGateway {
ExternalOrderId submit(Order o);
OrderStatus checkStatus(ExternalOrderId id);
}
// 2. ACL 实现——翻译 + 错误兜底
public class LegacyOrderAdapter implements ExternalOrderGateway {
private final LegacySOAPClient legacy; // 脏接口
private final Clock clock;
public ExternalOrderId submit(Order o) {
try {
LegacyRequest req = LegacyRequest.fromDomain(o);
LegacyResponse resp = legacy.placeOrder(req);
return new ExternalOrderId(resp.getOrderNo());
} catch (SOAPFaultException e) {
if (e.isRetriable()) {
return retryWithBackoff(() -> legacy.placeOrder(req));
}
throw new ExternalSystemException(e); // 翻译成本 BC 领域异常
}
}
public OrderStatus checkStatus(ExternalOrderId id) {
// 外部的 6 状态机翻译成本 BC 的 4 状态机
LegacyStatus legacy = legacy.queryStatus(id.value());
return LegacyStatusTranslator.toDomain(legacy);
}
}
// 3. 配置注入(生产 / 测试可换)
@Configuration
class ACLConfig {
@Bean
ExternalOrderGateway externalOrderGateway(
LegacySOAPClient legacy, Clock clock) {
return new LegacyOrderAdapter(legacy, clock);
}
}场景 2:处理脏数据(最常见的真实场景)
外部系统字段混乱:customerNo 有时是 String、有时是 Long、有时空。
public class LegacyCustomerAdapter {
CustomerId parseCustomerId(Object raw) {
if (raw == null) {
throw new CustomerIdMissingException(); // 不静默吞
}
if (raw instanceof String s) {
if (s.isBlank()) throw new CustomerIdMissingException();
return new CustomerId(s.trim());
}
if (raw instanceof Number n) {
return new CustomerId(String.valueOf(n.longValue()));
}
throw new UnexpectedCustomerIdFormatException(raw.getClass());
}
// 兜底 + 限流:脏数据不能拖垮本 BC
Customer enrich(Customer c) {
if (c.name() == null) {
c = c.withName("[UNKNOWN]");
metrics.increment("acl.enrich.fallback");
}
return c;
}
}场景 3:上游 BC 字段变了
上游 v1:{ orderId, total }
上游 v2:{ orderId, total, taxAmount, discount } ← 加字段
上游 v3:{ orderId, total, tax } ← 改字段有 ACL:只改 Adapter 的 fromV3() 解析方法,领域层 0 改动。 没有 ACL:全 BC 几百个调用点 × 3 次外部升级 = 几千行 diff。
V2 B7 段 4 演化视角:ACL 是"演化隔离带"——外部 BC 升级频率再高,本 BC 不受影响。
场景 4:ACL 的测试策略
class LegacyOrderAdapterTest {
// 只测翻译逻辑,不依赖 SOAP
@Test
void translates_legacy_response_to_domain_order() {
LegacyOrderDTO dto = TestData.legacyOrderWith(
"ORDER-001", "CUST-9", List.of("ITEM-A", "ITEM-B"));
Order order = adapter.toDomainOrder(dto);
assertThat(order.id()).isEqualTo(new OrderId("ORDER-001"));
assertThat(order.items()).hasSize(2);
}
}
// 真 SOAP 集成测试单独跑(标记 @Tag("integration"))常见误区
- "ACL = 转换器"——错。ACL 是翻译 + 适配 + 隔离 + 兜底,不只是格式转换
- "ACL 越薄越好"——错。ACL 越厚越安全——所有外部"怪东西"都在 ACL 内消化,领域层越干净
- "用 MapStruct 自动映射就行"——错。MapStruct 处理不了"格式异常 → 业务异常"的翻译,也处理不了"字段语义变化"
- "ACL 只在接入时写一次"——错。外部会升级,ACL 是持续维护的代码,要有 owner、有测试、有变更流程
- "ACL 内部可以反向污染本 BC 命名"——错。ACL 内部方法名可以"靠近外部"(如
legacySubmit),但对外必须用本 BC 通用语言
关键洞察
ACL = "本 BC 与外部世界之间的免疫系统"。
没有 ACL = 外部的任何变化都是本 BC 的债。 有 ACL = 外部再乱,本 BC 领域代码永远干净、永远独立。
V2 B3 段 5"团队可拥有"的工程纪律:每个 BC 至少有一个 ACL 端口, 所有外部依赖都从端口进。判断标准 = 搜代码:
import了 外部 SDK 的地方,都应该被 ACL 包起来。
相关链接
- Shared Kernel——另一种跨 BC 关系(共享内核)
- Customer-Supplier——上下游关系里的 ACL 角色
- Context Map——ACL 在 Context Map 中的标注
- Bounded Context——ACL 守护的边界
- [V3 涌现作为] · 分解——ACL 是分解的边界接口
- [V3 涌现作为] · 抽象——ACL 是抽象端口的具体形态
- [V3 涌现之时] · 演化——ACL 让演化隔离