python设计模式–适配器模式(Adapter Pattern)

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)ConsoleLoggerprint_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 一个适配器适配多个被适配者?

通常是一个适配器类对应一种被适配者;若多种实现都要适配到同一目标接口,就写多个适配器类(如上面的 FileLoggerAdapterConsoleLoggerAdapter)。

10.3 目标接口怎么定?

由“客户”和“业务需求”决定:客户希望怎么调(方法名、参数、返回值),目标接口就长什么样;适配器负责把目标接口映射到被适配者的接口。


十一、小结

  • 适配器模式:在目标接口被适配者之间加一层,把客户对目标接口的调用转译成对被适配者的调用,从而在不改(或少改)双方的前提下让接口兼容。
  • 三个角色:目标(Target)、被适配者(Adaptee)、适配器(Adapter);常用对象适配器(适配器持有被适配者实例)。
  • 典型用途:对接第三方/遗留接口、统一多种不同实现、新旧系统对接。
  • 实现要点:适配器实现目标接口,在方法内做参数/返回值转换并调用被适配者。

建议先写一个“方法名不同”的简单适配器(如 request -> get),再写一个带“参数/返回值转换”的(如支付 元/分、dict -> bool),最后试“统一多种实现”(如多种日志),这样对适配器模式会掌握得比较扎实。

发表评论