适配器模式练习——三步巩固
按《Python 设计模式——适配器模式》文末建议,分三步练习:
① 方法名不同的简单适配器(如 request → get);
② 带参数/返回值转换的适配器(如支付 元/分、dict → bool);
③ 统一多种实现(如多种日志)。
每步都有完整代码和验证要点,可直接复制运行。
练习一:方法名不同的简单适配器(request → get)
目的
体会“目标接口”和“被适配者”只是方法名不同,适配器只做“转发”:实现目标方法,内部调用被适配者的方法即可,不做单位或格式转换。
1.1 目标接口(客户期望的)
客户代码只认“能调 request(url) 的对象”:
from abc import ABC, abstractmethod
class HttpClient(ABC):
"""目标接口:客户依赖 request(url)"""
@abstractmethod
def request(self, url: str) -> str:
pass
1.2 被适配者(已有、方法名是 get)
class LegacyHttp:
"""老库:只有 get(url),没有 request"""
def get(self, url: str) -> str:
return f"[LegacyHttp.get] 请求了 {url}"
1.3 适配器:实现 request,内部调 get
class LegacyHttpAdapter(HttpClient):
def __init__(self):
self._legacy = LegacyHttp()
def request(self, url: str) -> str:
return self._legacy.get(url)
1.4 客户代码与验证
def client_code(http: HttpClient):
result = http.request("https://api.example.com")
print(result)
adapter = LegacyHttpAdapter()
client_code(adapter) # 应输出:[LegacyHttp.get] 请求了 https://api.example.com
验证要点:
- 客户只依赖
HttpClient,只调request(url)。 - 实际执行的是
LegacyHttp.get,说明适配器正确“转译”了方法名。
1.5 练习一完整可运行代码
# 练习一:方法名不同 request -> get
from abc import ABC, abstractmethod
class HttpClient(ABC):
@abstractmethod
def request(self, url: str) -> str:
pass
class LegacyHttp:
def get(self, url: str) -> str:
return f"[LegacyHttp.get] 请求了 {url}"
class LegacyHttpAdapter(HttpClient):
def __init__(self):
self._legacy = LegacyHttp()
def request(self, url: str) -> str:
return self._legacy.get(url)
def client_code(http: HttpClient):
print(http.request("https://api.example.com"))
if __name__ == "__main__":
client_code(LegacyHttpAdapter())
练习二:带参数/返回值转换的适配器(支付:元/分、dict → bool)
目的
适配器不仅要“转发”,还要做参数转换(元 → 分)和返回值转换(dict → bool),让客户始终用“元”和“是否成功”的布尔值,而被适配者继续用“分”和 dict。
2.1 目标接口
class Payment(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
"""amount 单位:元。返回是否成功。"""
pass
2.2 被适配者(第三方:分 + dict)
class ThirdPartyPay:
"""第三方:金额单位是分(int),返回 {"ok": bool, "msg": str}"""
def charge(self, cents: int) -> dict:
if cents <= 0:
return {"ok": False, "msg": "invalid amount"}
return {"ok": True, "msg": "success"}
2.3 适配器:元→分、dict→bool
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) # 返回值转换:dict -> bool
2.4 客户代码与验证
p = ThirdPartyPayAdapter()
print(p.pay(99.5)) # True
print(p.pay(0)) # False
print(p.pay(-1)) # False
验证要点:
- 客户始终用“元”(float)调用
pay(amount),得到bool。 - 适配器内部把 99.5 转成 9950 分调用
charge,再把{"ok": True}转成True。
2.5 练习二完整可运行代码
# 练习二:参数/返回值转换 元->分 dict->bool
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
class ThirdPartyPay:
def charge(self, cents: int) -> dict:
if cents <= 0:
return {"ok": False, "msg": "invalid amount"}
return {"ok": True, "msg": "success"}
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)
if __name__ == "__main__":
p = ThirdPartyPayAdapter()
print(p.pay(99.5)) # True
print(p.pay(0)) # False
练习三:统一多种实现(多种日志)
目的
业务代码只依赖统一接口 Logger.log(level, message);现有两种实现接口不同:
FileLogger.write(level, msg)ConsoleLogger.print_msg(msg)(没有 level)
为两者各写一个适配器,都实现 Logger,这样客户可以用同一套代码切换“写文件”或“打控制台”。
3.1 目标接口
class Logger(ABC):
@abstractmethod
def log(self, level: str, message: str):
pass
3.2 被适配者 A:文件日志
class FileLogger:
def __init__(self, path: str):
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")
3.3 被适配者 B:控制台(无 level)
class ConsoleLogger:
def print_msg(self, msg: str):
print(msg)
3.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}")
3.5 客户只依赖 Logger
def do_work(logger: Logger):
logger.log("INFO", "开始处理")
logger.log("ERROR", "某处出错")
# 同一套业务代码,换适配器就换实现
do_work(ConsoleLoggerAdapter())
do_work(FileLoggerAdapter("app.log")) # 会写入文件 app.log
验证要点:
do_work只调logger.log(level, message),不关心底层是文件还是控制台。- 换传入的适配器即可切换“多种实现”,无需改业务代码。
3.6 练习三完整可运行代码
# 练习三:统一多种实现(多种日志)
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, level: str, message: str):
pass
class FileLogger:
def __init__(self, path: str):
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")
class ConsoleLogger:
def print_msg(self, msg: str):
print(msg)
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):
self._logger.print_msg(f"[{level}] {message}")
def do_work(logger: Logger):
logger.log("INFO", "开始处理")
logger.log("ERROR", "某处出错")
if __name__ == "__main__":
do_work(ConsoleLoggerAdapter())
do_work(FileLoggerAdapter("app.log"))
三步汇总与自检
| 练习 | 适配器做的事 | 关键点 |
|---|---|---|
| 一 | 只做方法名映射 | request(url) → get(url),参数和返回值不变 |
| 二 | 参数 + 返回值转换 | 元→分、dict→bool,客户与第三方接口解耦 |
| 三 | 统一多种实现 | 多个被适配者、多个适配器,同一目标接口 |
自检问题:
- 客户代码应依赖谁?目标接口(如 HttpClient、Payment、Logger),不直接依赖被适配者。
- 适配器必须实现什么?目标接口,并在实现里把调用转给被适配者(必要时做参数/返回值转换)。
- “统一多种实现”时,是一个适配器适配多个类,还是多个适配器?多个适配器,每个被适配者对应一个适配器,都实现同一个目标接口。
按顺序做完这三步,再对照文档里的完整代码跑一遍,对适配器模式会掌握得比较扎实。