设计模式 21:接口转换如何保留语义
订单侧的报价单位是分;一个模拟旧 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_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 | |
共同断言对两个 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。这里的货币单位纯属合成协议,不能移用到真实供应商。
- 新增第三个模拟供应商,金额改用十进制字符串、失败改用异常;先让已有两协议的共同合同保持,再验证新适配器的金额、溢出、拒绝与重试。
- 同一键被用于 200 与 300 分时会发生什么?先写揭示当前边界的反例,再决定在适配器还是 SDK 模拟端拒绝冲突;不可在没有验证的情况下宣称生产幂等。
本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。
参考资料
- GoF 原书公开图书馆 PDF,4.1 Adapter,目录页标注起始页 157:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- GoF Adapter 原书摘录,Pearson/InformIT:https://www.informit.com/articles/article.aspx?p=1398600 。
- Java SE 21
Math.multiplyExactJavadoc:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#multiplyExact(long,long) 。 - 实验代码:
examples/design-patterns/labs/21/。
上一节:20 唯一实例属于哪个范围;下一节:22 两条变化维度如何分开。





