订单侧的报价单位是分;一个模拟旧 SDK 的 charge(key, milli) 以十分之一分计量。若把 200 分原样交过去,接口调用并不报错,实际只记了 200 milli,订单端读取结果得到 20 分。这类故障不是改方法名就能修好:金额单位、拒绝原因和重复请求的语义都要一起核对。

边界上转换,而不是到处乘十

labs/21 的 Before 恰好复现错误:pay("a", 200) 返回 Payment(20),模拟 SDK 累计收费 200 milli。After 实现调用者定义的 PaymentPort,先检查非空请求键与正金额,用 Math.multiplyExact(cents, 10) 防止乘法溢出,再调用旧 SDK;将 NO_FUNDS 转成明确异常而不是金额为 0 的“成功”结果。失败断言还核对模拟累计收费保持 0。

1
2
3
4
Payment pay(String key, int cents) {
validate(key, cents);
return interpret(sdk.charge(key, Math.multiplyExact(cents, 10)));
}

同一键与金额重试两次,只记一次 2_000 milli,返回的都是 Payment(200)。这是本地单线程 HashMap 的教学约定:键在本地模拟 SDK 内保存已处理响应。没有验证不同金额复用同一键、并发、进程重启或外部支付方幂等性,不能据此给真实支付提供“恰好一次”保证。

Alternative 在一个调用位置内联验证和转换,若只有这一个边界,它避免新增接口;After 在需要替换旧 SDK 或多个调用者共享同一适配边界时更有意义。Adapter 的意图是让已有不兼容协议符合调用者合同,不等于 Decorator(同接口叠加行为)、Proxy(控制访问)、Facade(简化多个服务入口)或 Bridge(两条独立变化维度的组合)。它们都可能“包一层”,决定类型的不是层数,而是具体变化需求。

方案 200 分的收费 失败与重复请求
Before 错误地记 200 milli 未区分拒绝码
After 转成 2_000 milli 拒绝码显式抛错;同键重试由 SDK 模拟去重
Alternative 同样转成 2_000 milli 边界逻辑留在单一调用点

第二种协议复用同一个调用方合同

第二个模拟 SDK 的 submit(units, reference) 使用百分之一分,成功与否用布尔值返回,参数顺序也不同。HundredthAdapter 仍实现订单侧的 PaymentPort:先验证键和正金额,用 Math.multiplyExact(cents, 100) 转单位,再把拒绝映射为同一类异常。Math.multiplyExact 的溢出抛错约定来自 Java 21 Math 文档,供应商单位和限额均为本地教学合同。

1
2
3
Scenario.PaymentPort first = new Scenario.After(new Scenario.LegacySdk());
Scenario.PaymentPort second = new Scenario.HundredthAdapter(new Scenario.HundredthSdk());
Scenario.Payment payment = second.pay("order-a", 200);

共同断言对两个 PaymentPort 执行完全相同的调用:200 分成功并返回 Payment(200),同键同金额重试不重复收费;空键、0 分、乘法溢出均失败,1001 分被模拟余额规则拒绝。两个 SDK 分别累计 2000 个十分之一分和 20000 个百分之一分。调用方无需根据供应商改变金额单位或解释不同响应。

两种协议都在单线程内存 Map 保存结果,尚未提供同键不同金额的冲突检测、持久化去重或网络重试机制。本例支持替换协议的金额与错误合同,不能扩大为真实支付安全证明。只有一个调用位置时,原先的内联方案仍可用;出现第二种协议后,适配器将供应商差异约束在两处边界实现里。

flowchart LR
    Caller[业务调用方 cents] --> Port[PaymentPort]
    A[After 适配器] -.实现.-> Port
    B[HundredthAdapter] -.实现.-> Port
    A --> Legacy[LegacySdk 十分之一分]
    B --> Hundredth[HundredthSdk 百分之一分]

实验与练习

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/21 -am test、./mvnw -B -ntp verify。金额、键、异常、原始输出和退出码见 examples/design-patterns/evidence/21/RUN.md。这里的货币单位纯属合成协议,不能移用到真实供应商。

  1. 新增第三个模拟供应商,金额改用十进制字符串、失败改用异常;先让已有两协议的共同合同保持,再验证新适配器的金额、溢出、拒绝与重试。
  2. 同一键被用于 200 与 300 分时会发生什么?先写揭示当前边界的反例,再决定在适配器还是 SDK 模拟端拒绝冲突;不可在没有验证的情况下宣称生产幂等。

本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。

参考资料

上一节:20 唯一实例属于哪个范围;下一节:22 两条变化维度如何分开。