PHP设计模式之适配器模式(Adapter)原理与用法详解

发布时间:2026/10/9 13:19:08
PHP设计模式之适配器模式(Adapter)原理与用法详解 前言适配器模式Adapter Pattern解决的是一个非常具体的问题你需要的接口和手上已有的类的接口对不上。它通过增加一个中间类把一个类的接口「翻译」成客户端期望的另一种接口。生活中最贴切的类比是电源转接头笔记本要的是某个形状的插头墙上只有另一种插座转接头不改变电只改变「接口形状」。围绕它有三个常见误解把适配器当成装饰器Decorator。装饰器是在保持同一接口的前提下增强行为比如给日志加一层「同时写文件」适配器改变接口形态让不兼容的东西能对接。判断标准很简单客户端代码调用前后的方法签名是否一致——一致是装饰不一致是适配。以为适配器可以随便加。适配器是「隔离层」它存在的意义是把不可控的外部依赖挡在一个角落。如果给自己的每个类都加一层适配器那只是把代码量翻倍并没有隔离任何风险。以为必须继承被适配的类。这类「类适配器」在 PHP 里存在明显短板下面有对比表实际项目里用的是对象适配器——持有被适配对象的实例把调用委托过去。适配器最常见的真实战场是接入第三方 SDK不同厂商的方法名、参数顺序、返回结构、错误码各不相同而业务代码不应该认识它们。本文用「第三方日志库」和「短信服务商」两个例子讲清对象适配器的写法与边界。一、三个角色角色职责本文示例目标接口Target业务代码期望的接口由你自己的项目定义AppLogger适配者Adaptee已有的、接口不兼容的类通常来自第三方VendorLogger适配器Adapter实现目标接口把调用翻译成对适配者的调用VendorLoggerAdapter关键约束是业务代码只依赖目标接口适配者与适配器都藏在边界之外。这样替换第三方时只需要改适配器甚至只换一个配置项业务代码不动。二、对象适配器实战把第三方日志库接进项目假设项目内部约定了一个日志接口而引入的第三方日志库方法名、级别命名、参数结构都不一样?php // 适用于 PHP 8.0match 需要 8.0declare(strict_types1);// 【目标接口】项目内部统一使用它interface AppLogger{public function log(string $level, string $message, array $context []): void;}// 【适配者】第三方库方法名不同、没有 context 参数、级别用 warn/error 这类词final class VendorLogger{public function writeLine(string $severity, string $text): void{printf([%s] %s\n, strtoupper($severity), $text);}}// 【适配器】实现目标接口把调用翻译后委托给适配者final class VendorLoggerAdapter implements AppLogger{private VendorLogger $vendor;public function __construct(VendorLogger $vendor){$this-vendor $vendor;}public function log(string $level, string $message, array $context []): void{// 1) 翻译级别把项目内的级别词映射到第三方认识的那几个$severity match (strtolower($level)) {warning warn,error, critical, alert, emergency error,default info,};// 2) 翻译参数第三方不支持结构化 context用 JSON 拼进文本if ($context ! []) {$message . . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);}// 3) 委托真正的活由适配者干$this-vendor-writeLine($severity, $message);}}// 业务代码只认识 AppLoggerfunction registerUser(AppLogger $logger, string $name): void{$logger-log(warning, 库存不足, [sku A-100, left 2]);$logger-log(info, 用户注册, [name $name]);}registerUser(new VendorLoggerAdapter(new VendorLogger()), ann);如果项目跑在 PHP 7.x 上把match换成switch即可PHP 7 没有match?php // 适用于 PHP 7.0 的等价写法$severity info;switch (strtolower($level)) {case warning:$severity warn;break;case error:case critical:case alert:case emergency:$severity error;break;}三、对象适配器 vs 类适配器维度类适配器继承适配者对象适配器持有适配者实现方式extends VendorLogger implements AppLogger构造函数注入适配者内部委托PHP 里的可行度低——单继承继承位置被占掉高项目里基本都用这种适配多个适配者做不到可以组合多个对象适配适配者的子类被具体类绑死可以注入不同实现测试友好度差无法替换被继承的行为好可注入替身对象PHP 不支持多重继承因此「类适配器」只在极少数场景下可用。这也是为什么在 PHP 里谈适配器默认指的都是对象适配器。顺带澄清trait是代码复用机制不是「多继承的替代品」用它来实现接口适配只会让方法来源变得难以追踪。四、统一多个厂商适配器最典型的用法同一个能力接入多家供应商时适配器的价值最明显。业务代码只面向统一的接口每家厂商的差异都被适配器吃掉?php // 适用于 PHP 8.0declare(strict_types1);// 【目标接口】统一契约成功返回消息 ID失败抛异常interface SmsSender{public function send(string $phone, string $text): string;}// 【适配者 A】第三方 SDK数组进、数组出用 ok 字段表示成败final class VendorASdk{/** return array{ok: bool, id?: string, err?: int} */public function sendSms(array $params): array{// 真实 SDK 会发 HTTP 请求这里用固定结构示意return [ok true, id A-10086];}}// 【适配器 A】把数组返回值翻译成「字符串或异常」final class VendorASender implements SmsSender{public function __construct(private VendorASdk $sdk){}public function send(string $phone, string $text): string{$result $this-sdk-sendSms([to $phone,body $text,format json,]);if (($result[ok] ?? false) ! true) {// 翻译错误抛出项目自己的异常不把第三方的返回结构泄漏出去throw new \RuntimeException(短信发送失败厂商错误码: . ($result[err] ?? unknown));}return (string)$result[id];}}// 业务代码完全不知道背后是哪家厂商function notify(SmsSender $sender, string $phone): string{return $sender-send($phone, 您的验证码是 123456);}echo notify(new VendorASender(new VendorASdk()), 13800000000), PHP_EOL;新增厂商时只需要再写一个VendorBSender implements SmsSender业务代码一行都不用改——这就是适配器在「供应商替换」场景里最实际的价值。常见坑点❌ 把适配器当装饰器用让适配器实现同一个接口然后在里面加缓存、加重试。✅ 装饰器保持接口不变并增强行为适配器负责转换接口两种意图混在一起会让调用方分不清「谁在做什么」。要加缓存或重试再套一层装饰器。❌ 在适配器里写业务判断比如「金额超过 100 就要人工审核」。✅ 适配器只做「翻译 委托」业务规则属于领域层混进来会让适配器无法复用到别的场景。❌ 目标接口的方法签名里出现第三方的类型public function send(VendorAResult $r): VendorAResponse。✅ 目标接口只使用自己项目的类型字符串、数组、自定义异常一旦泄漏第三方类型隔离层就失效了。❌ 适配器里catch (\Throwable $e) { return null; }把第三方的失败吞掉。✅ 应把第三方异常翻译成项目自己的异常再抛出保留足够定位问题的信息错误码、请求标识但要避免把敏感信息写进日志。❌ 在适配器内部直接new VendorASdk()或者用单例硬编码第三方对象。✅ 通过构造函数注入如上面的__construct(private VendorASdk $sdk)测试时可以注入替身对象。❌ 为了「解耦」给项目自己的每个类都套一层适配器。✅ 适配器应该只出现在不可控的边界上第三方 SDK、老系统接口、外部队列内部类之间用接口直接协作即可。❌ 认为适配器里应该顺手升级第三方库的调用方式。✅ 适配器正是升级的隔离点第三方换代时改动被限制在适配器内部。但升级要连带更新翻译逻辑方法名、错误码、字段名都可能变并补上对应的测试。❌ 用已经废弃或移除的扩展去实现底层能力例如仍在用mcrypt_*做加解密。✅mcrypt_*从 PHP 7.2 起废弃、已不适合新项目可逆加密改用sodium_*或带随机 nonce 的openssl_encrypt()消息认证用hash_hmac()配合hash_equals()。适配器要适配的是接口不是把过时实现换个包装继续用。总结关注点结论解决什么让接口不兼容的两方协作把第三方差异隔离在边界上三个角色目标接口自己定义、适配者第三方、适配器实现目标并委托首选形态对象适配器持有 委托PHP 单继承下类适配器基本不实用与装饰器的区别装饰器保持接口不变并增强适配器改变接口做转换与工厂的关系工厂负责「创建哪个适配器」适配器负责「怎么转换」适配器的边界只出现在不可控的外部依赖处内部类之间不需要适配器的价值来自一句话把「会变的东西」关进一个类里。第三方的 SDK 会升级、错误码会变、供应商会被替换而业务代码只依赖自己定义的接口——这个接口就是那道墙适配器则是墙上唯一的门。用它的时候要守住两条线目标接口里不出现第三方类型适配器里不写业务逻辑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询