必须用适配器而非直接改调用方:当旧模块已上线、多依赖、高覆盖,且新接口不兼容时,适配器可隔离变化、避免硬编码分支。

什么时候必须用适配器,而不是直接改调用方?
当你面对一个已上线、有大量测试覆盖、被多个模块依赖的旧支付模块(比如 LegacyPaymentService.make_payment(order_id, amount)),而新接入的 Stripe SDK 要求你传 amount_cents 和 currency、返回 PaymentIntent 对象时——别碰原模块。改调用方等于在每个业务入口加 if/else,很快会演变成 if payment_provider == "legacy": ... elif payment_provider == "stripe": ...,这已经不是适配,是硬编码分支。
适配器的触发信号很明确:已有类不能动、接口不一致、且你希望客户端代码完全无感地切换实现。
对象适配器怎么写才不容易翻车?
Python 中唯一推荐的方式是对象适配器(组合),不是类适配器(多重继承)。后者看似简洁,但一旦 LegacyPaymentService 和你的目标接口有同名方法(比如都定义了 close()),继承链会引发静默覆盖或 MRO 冲突,调试成本远高于多写一行 self.legacy = legacy。
实操要点:
-
Adapter类必须显式继承目标抽象基类(如PaymentProcessor)或协议(Protocol),否则类型检查器(mypy)和 IDE 无法推导接口一致性 - 构造函数只接收被适配对象(
legacy_service: LegacyPaymentService),不接收任何转换逻辑参数(如汇率、默认币种)——这些应作为实例属性或配置项注入,而非硬编码在__init__里 - 所有字段转换(如
amount→amount * 100)、异常映射(把StripeError转成统一的PaymentError)必须集中在pay()方法体内,不要分散到辅助方法里
如何让适配器支持运行时切换?
真实项目中,你往往需要灰度:部分订单走旧系统,部分走 Stripe。这时适配器本身不该承担路由逻辑,而是由工厂或依赖注入容器控制实例创建。
错误做法:OldToNewAdapter(pay_method="stripe") —— 把适配器变成条件分支容器
正确做法:
- 定义统一接口
PaymentProcessor(含pay(self, order_id: str, amount: float) -> bool) - 为每个实现写独立适配器:
LegacyAdapter(legacy_service)、StripeAdapter(stripe_client) - 在工厂函数中按配置返回对应实例:
def get_processor(env: str) -> PaymentProcessor:,里面用if env == "prod_legacy": return LegacyAdapter(...)
这样单元测试可以分别 mock 每个适配器,不会因为一个 if 分支崩掉整个测试套件。
最容易被忽略的坑:异常和日志透传
适配器常被当成“胶水”,结果把关键上下文吞掉了。比如 LegacyPaymentService.make_payment() 失败时抛出 LegacyNetworkTimeout,而你在 LegacyAdapter.pay() 里只写 except Exception: return False——上游完全不知道是超时还是金额校验失败。
必须做三件事:
- 捕获被适配者抛出的具体异常类型(如
LegacyAuthError),转为统一异常子类(如PaymentAuthError),并保留原始__cause__ - 在适配器内部打日志时,必须包含原始调用参数(
order_id,amount)和被适配者的返回值/异常,否则线上排查时只能看到“适配器失败”,看不到失败源头 - 如果被适配者有重试机制(如三次 HTTP 重试),适配器不应再包一层重试——否则重试次数会指数级放大
适配器的边界就在这里:它只做接口形状转换,不改变语义、不新增行为、不隐藏细节。越靠近这个原则,后期维护时就越少想删掉它重写一遍。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











