Python 设计模式——适配器模式(Adapter Pattern)详解
本文面向零基础新手,从概念到两种适配器、从简单示例到实际场景,全方位讲解适配器模式。
一、什么是适配器模式?
1.1 通俗理解
适配器模式是一种结构型设计模式,核心思想是:
有一个“客户”期望的接口(比如某种调用方式),还有一个“已有类”能干活但接口不兼容(方法名不同、参数不同、返回格式不同)。适配器站在中间:对外满足客户期望的接口,对内把请求“转译”成已有类能理解的方式,从而让客户能用统一的方式使用原本不兼容的类。
可以这样类比:
- 现实中的适配器:中国电器是两脚插头,日本插座是两孔但电压/形状不同,你买一个“转换插头”(适配器):一头插日本插座,一头提供中国电器能插的接口。电器(客户)只认“我能插的接口”,适配器负责和插座(已有系统)打交道。
- 代码里:你的代码期望调用
pay(amount),但第三方支付库提供的是charge(price_in_cents)。写一个适配器类,实现你规定的pay(amount),内部把amount转成“分”再调用charge(price_in_cents),这样你的代码不用改,就能用上第三方库。
所以,适配器模式解决的是:接口不兼容时,在不改(或尽量少改)双方代码的前提下,让“客户”用统一接口去使用“已有类”。
1.2 为什么需要适配器?
场景一:使用第三方库或遗留代码
你希望统一用“我的接口”(如 send(message)),但老系统或第三方提供的是别的接口(如 write_text(msg)、emit(msg))。改第三方或老系统不现实,那就加一层适配器,把你的调用“转译”过去。
场景二:统一多种实现
系统里有多套实现(如多种日志库、多种支付渠道),接口各不相同。你希望业务代码只依赖“统一接口”(如 log(level, msg)),这时可以给每种实现各写一个适配器,都实现统一接口,内部再转成各自的方法调用。
场景三:新旧系统对接
新模块按新接口设计,老模块按老接口;对接时不能把老模块全重写,就通过适配器让新模块“以为”在调新接口,实际适配器去调老模块。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 要用一个接口不兼容的类 | 不能改这个类(第三方、遗留),但希望按你的接口用。 |
| 统一多种不同接口的实现 | 多种日志、多种支付、多种存储,对外只暴露一套接口。 |
| 新旧系统/模块对接 | 新代码只依赖“目标接口”,适配器负责调老接口。 |
简单记忆:两边接口对不上、又不想大改两边代码时,中间加一个“转译层”,就是适配器。
二、适配器模式的结构(三个角色)
| 角色 | 说明 |
|---|---|
| 目标(Target) | 客户所依赖的接口,即“我们期望的调用方式”(如 pay(amount)、log(msg))。 |
| 被适配者(Adaptee) | 已有的、能干活但接口不兼容的类(如第三方支付类、老日志类)。 |
| 适配器(Adapter) | 实现“目标”接口,内部持有一个“被适配者”,把客户对目标接口的调用转成对被适配者的调用。 |
关系可以理解为:
- 客户 只依赖 目标接口,调用目标的某个方法。
- 适配器 实现目标接口,内部把请求转译给 被适配者,得到结果后再按目标接口的约定返回给客户。
三、两种实现方式:对象适配器 vs 类适配器
- 对象适配器:适配器类持有一个被适配者的实例(组合),在方法里调用这个实例。Python 里常用这种方式,灵活、符合“组合优于继承”。
- 类适配器:适配器继承被适配者,同时实现目标接口(多继承)。Python 也支持,但依赖多继承,有时不如对象适配器清晰。
本文示例以对象适配器为主;类适配器会在后面简要对比。
四、示例一:最简单的适配器(方法名不同)
需求:你的代码统一调用 request(url),但有一个老类 LegacyHttp 只有 get(url),没有 request。写一个适配器,对外提供 request(url),内部转成 get(url)。
4.1 目标接口(客户期望的)
from abc import ABC, abstractmethod
class HttpClient(ABC):
"""目标接口:客户只依赖这个"""
@abstractmethod
def request(self, url: str) -> str:
pass
4.2 被适配者(已有、接口不同)
class LegacyHttp:
"""老库:只有 get,没有 request"""
def get(self, url: str) -> str:
return f"[LegacyHttp.get] 请求了 {url}"
4.3 适配器(实现目标,内部调被适配者)
class LegacyHttpAdapter(HttpClient):
"""适配器:实现 request,内部转成 get"""
def __init__(self):
self._legacy = LegacyHttp()
def request(self, url: str) -> str:
return self._legacy.get(url)
4.4 客户代码(只依赖目标接口)
def client_code(http: HttpClient):
print(http.request("https://api.example.com"))
# 使用适配器后,客户用统一接口
adapter = LegacyHttpAdapter()
client_code(adapter) # [LegacyHttp.get] 请求了 https://api.example.com
要点:客户只知道 HttpClient.request(url),不关心底层是 LegacyHttp.get 还是别的;以后若要换成别的 HTTP 实现,只需换一个实现 HttpClient 的类(或再写一个适配器)。
五、示例二:参数与返回格式不同(支付接口)
需求:你的系统统一用“人民币元”和接口 pay(amount: float) -> bool;某个第三方支付类用的是“分”(整数)且方法为 charge(cents: int) -> dict。适配器要做两件事:把“元”转成“分”,把返回的 dict 转成 bool。
5.1 目标接口
class Payment(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
"""amount 单位:元。返回是否成功。"""
pass
5.2 被适配者(第三方风格)
class ThirdPartyPay:
"""第三方:金额单位是分(int),返回 dict"""
def charge(self, cents: int) -> dict:
# 模拟:返回 {"ok": True/False, "msg": "..."}
return {"ok": True, "msg": "success"} if cents > 0 else {"ok": False, "msg": "invalid"}
5.3 适配器(单位转换 + 返回值转换)
class ThirdPartyPayAdapter(Payment):
def __init__(self):
self._pay = ThirdPartyPay()
def pay(self, amount: float) -> bool:
cents = int(round(amount * 100)) # 元 -> 分
result = self._pay.charge(cents)
return result.get("ok", False)
5.4 使用
p = ThirdPartyPayAdapter()
print(p.pay(99.5)) # True
print(p.pay(0)) # False
客户始终用“元”和 pay(amount),适配器负责和“分”与 charge 打交道。
六、示例三:统一多种实现(多种日志)
需求:业务代码统一用 log(level, message);现有两种实现:FileLogger 写文件用 write(level, msg),ConsoleLogger 用 print_msg(msg) 且没有 level。为两者各写一个适配器,都实现统一的 Logger 接口。
6.1 目标接口
class Logger(ABC):
@abstractmethod
def log(self, level: str, message: str):
pass
6.2 被适配者 A:文件日志
class FileLogger:
def __init__(self, path):
self.path = path
def write(self, level: str, msg: str):
with open(self.path, "a", encoding="utf-8") as f:
f.write(f"[{level}] {msg}n")
6.3 被适配者 B:控制台(无 level)
class ConsoleLogger:
def print_msg(self, msg: str):
print(msg)
6.4 两个适配器
class FileLoggerAdapter(Logger):
def __init__(self, path: str):
self._logger = FileLogger(path)
def log(self, level: str, message: str):
self._logger.write(level, message)
class ConsoleLoggerAdapter(Logger):
def __init__(self):
self._logger = ConsoleLogger()
def log(self, level: str, message: str):
# 控制台没有 level,把 level 拼进消息
self._logger.print_msg(f"[{level}] {message}")
6.5 客户只依赖 Logger
def do_work(logger: Logger):
logger.log("INFO", "开始处理")
logger.log("ERROR", "某处出错")
do_work(FileLoggerAdapter("app.log"))
do_work(ConsoleLoggerAdapter())
业务代码不关心底层是文件还是控制台,只要传入一个实现了 Logger 的对象(可以是适配器,也可以是原生实现)。
七、示例四:鸭子类型下的“适配”(Python 特色)
在 Python 里,不一定要用抽象类定义“目标接口”;只要对象有所需的方法即可(鸭子类型)。适配器只要提供客户要调的方法即可。
例如客户只关心“能调用 send(msg) 的对象”:
class EmailSender:
def send_email(self, to: str, subject: str, body: str):
print(f"邮件 -> {to}, 主题: {subject}")
class EmailAdapter:
"""适配器:把 send(message) 转成 send_email(to, subject, body)"""
def __init__(self, sender: EmailSender, default_to: str):
self._sender = sender
self._default_to = default_to
def send(self, message: str):
self._sender.send_email(self._default_to, "通知", message)
# 客户只调 send(msg)
sender = EmailAdapter(EmailSender(), "user@example.com")
sender.send("你好")
这里没有显式的 Target 抽象类,但“目标”就是“有 send(message) 的对象”;适配器实现了这个约定,并转译给 EmailSender。
八、类适配器(多继承)简要说明
类适配器通过多继承同时“继承被适配者”和“实现目标接口”,在适配器内部用 self 直接调用被适配者的方法(因为 self 也是被适配者的子类)。
class LegacyHttp:
def get(self, url: str) -> str:
return f"[get] {url}"
class LegacyHttpClassAdapter(LegacyHttp, HttpClient):
"""类适配器:继承 LegacyHttp,并实现 HttpClient"""
def request(self, url: str) -> str:
return self.get(url) # 直接调继承来的 get
注意:多继承有顺序、命名冲突等问题;Python 里更常见、更清晰的是对象适配器(持有一个被适配者实例)。
九、适配器 vs 其他模式
| 模式 | 目的 | 与适配器的区别 |
|---|---|---|
| 适配器 | 让接口不兼容的类能被客户按统一接口使用 | 侧重“转译”接口,不改变被适配者的职责。 |
| 外观(Facade) | 为多个类/子系统提供简化的统一入口 | 外观是“简化调用”,可能合并多个调用;适配器是“接口转换”,通常一对一。 |
| 装饰器(Decorator) | 给对象增加行为(同接口) | 装饰器保持同一接口并增强功能;适配器是改成另一个接口。 |
十、常见问题与注意点
10.1 适配器要不要改被适配者?
尽量不改。适配器的价值就是“在不改被适配者(第三方、遗留)的前提下”让客户能用。若可以改被适配者,也可以考虑直接让被适配者实现目标接口(那就不是适配器了,是重构)。
10.2 一个适配器适配多个被适配者?
通常是一个适配器类对应一种被适配者;若多种实现都要适配到同一目标接口,就写多个适配器类(如上面的 FileLoggerAdapter 和 ConsoleLoggerAdapter)。
10.3 目标接口怎么定?
由“客户”和“业务需求”决定:客户希望怎么调(方法名、参数、返回值),目标接口就长什么样;适配器负责把目标接口映射到被适配者的接口。
十一、小结
- 适配器模式:在目标接口和被适配者之间加一层,把客户对目标接口的调用转译成对被适配者的调用,从而在不改(或少改)双方的前提下让接口兼容。
- 三个角色:目标(Target)、被适配者(Adaptee)、适配器(Adapter);常用对象适配器(适配器持有被适配者实例)。
- 典型用途:对接第三方/遗留接口、统一多种不同实现、新旧系统对接。
- 实现要点:适配器实现目标接口,在方法内做参数/返回值转换并调用被适配者。
建议先写一个“方法名不同”的简单适配器(如 request -> get),再写一个带“参数/返回值转换”的(如支付 元/分、dict -> bool),最后试“统一多种实现”(如多种日志),这样对适配器模式会掌握得比较扎实。