适配器模式练习

适配器模式练习——三步巩固

按《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,客户与第三方接口解耦
统一多种实现 多个被适配者、多个适配器,同一目标接口

自检问题

  1. 客户代码应依赖谁?目标接口(如 HttpClient、Payment、Logger),不直接依赖被适配者。
  2. 适配器必须实现什么?目标接口,并在实现里把调用转给被适配者(必要时做参数/返回值转换)。
  3. “统一多种实现”时,是一个适配器适配多个类,还是多个适配器?多个适配器,每个被适配者对应一个适配器,都实现同一个目标接口。

按顺序做完这三步,再对照文档里的完整代码跑一遍,对适配器模式会掌握得比较扎实。

发表评论