Skip to content

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 实现:

java
// 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、有时空。

java
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 的测试策略 ​

java
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"))

常见误区 ​

  1. "ACL = 转换器"——错。ACL 是翻译 + 适配 + 隔离 + 兜底,不只是格式转换
  2. "ACL 越薄越好"——错。ACL 越厚越安全——所有外部"怪东西"都在 ACL 内消化,领域层越干净
  3. "用 MapStruct 自动映射就行"——错。MapStruct 处理不了"格式异常 → 业务异常"的翻译,也处理不了"字段语义变化"
  4. "ACL 只在接入时写一次"——错。外部会升级,ACL 是持续维护的代码,要有 owner、有测试、有变更流程
  5. "ACL 内部可以反向污染本 BC 命名"——错。ACL 内部方法名可以"靠近外部"(如 legacySubmit),但对外必须用本 BC 通用语言

关键洞察 ​

ACL = "本 BC 与外部世界之间的免疫系统"。

没有 ACL = 外部的任何变化都是本 BC 的债。 有 ACL = 外部再乱,本 BC 领域代码永远干净、永远独立。

V2 B3 段 5"团队可拥有"的工程纪律:每个 BC 至少有一个 ACL 端口, 所有外部依赖都从端口进。判断标准 = 搜代码:import 了 外部 SDK 的地方,都应该被 ACL 包起来。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。