news 2026/10/22 16:15:14

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP设计模式之适配器模式(Adapter)原理与用法详解

前言


适配器模式(Adapter Pattern)解决的是一个非常具体的问题:你需要的接口,和手上已有的类的接口对不上。它通过增加一个中间类,把一个类的接口「翻译」成客户端期望的另一种接口。生活中最贴切的类比是电源转接头:笔记本要的是某个形状的插头,墙上只有另一种插座,转接头不改变电,只改变「接口形状」。


围绕它有三个常见误解:



  • 把适配器当成装饰器(Decorator)。装饰器是在保持同一接口的前提下增强行为(比如给日志加一层「同时写文件」);适配器改变接口形态,让不兼容的东西能对接。判断标准很简单:客户端代码调用前后的方法签名是否一致——一致是装饰,不一致是适配。

  • 以为适配器可以随便加。适配器是「隔离层」,它存在的意义是把不可控的外部依赖挡在一个角落。如果给自己的每个类都加一层适配器,那只是把代码量翻倍,并没有隔离任何风险。

  • 以为必须继承被适配的类。这类「类适配器」在 PHP 里存在明显短板(下面有对比表),实际项目里用的是对象适配器——持有被适配对象的实例,把调用委托过去。


适配器最常见的真实战场是接入第三方 SDK:不同厂商的方法名、参数顺序、返回结构、错误码各不相同,而业务代码不应该认识它们。本文用「第三方日志库」和「短信服务商」两个例子,讲清对象适配器的写法与边界。


一、三个角色




角色职责本文示例



目标接口(Target)业务代码期望的接口,由你自己的项目定义AppLogger

适配者(Adaptee)已有的、接口不兼容的类,通常来自第三方VendorLogger

适配器(Adapter)实现目标接口,把调用翻译成对适配者的调用VendorLoggerAdapter



关键约束是:业务代码只依赖目标接口,适配者与适配器都藏在边界之外。这样替换第三方时,只需要改适配器(甚至只换一个配置项),业务代码不动。


二、对象适配器实战:把第三方日志库接进项目


假设项目内部约定了一个日志接口,而引入的第三方日志库方法名、级别命名、参数结构都不一样:


<?php // 适用于 PHP 8.0+(match 需要 8.0)
declare(strict_types=1);

// 【目标接口】项目内部统一使用它
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);
}
}

// 业务代码只认识 AppLogger
function 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.0+
declare(strict_types=1);

// 【目标接口】统一契约:成功返回消息 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,业务代码一行都不用改——这就是适配器在「供应商替换」场景里最实际的价值。


常见坑点



  1. ❌ 把适配器当装饰器用:让适配器实现同一个接口,然后在里面加缓存、加重试。


✅ 装饰器保持接口不变并增强行为,适配器负责转换接口;两种意图混在一起会让调用方分不清「谁在做什么」。要加缓存或重试,再套一层装饰器。



  1. ❌ 在适配器里写业务判断(比如「金额超过 100 就要人工审核」)。


✅ 适配器只做「翻译 + 委托」;业务规则属于领域层,混进来会让适配器无法复用到别的场景。



  1. ❌ 目标接口的方法签名里出现第三方的类型:public function send(VendorAResult $r): VendorAResponse。


✅ 目标接口只使用自己项目的类型(字符串、数组、自定义异常);一旦泄漏第三方类型,隔离层就失效了。



  1. ❌ 适配器里catch (\Throwable $e) { return null; }把第三方的失败吞掉。


✅ 应把第三方异常翻译成项目自己的异常再抛出,保留足够定位问题的信息(错误码、请求标识),但要避免把敏感信息写进日志。



  1. ❌ 在适配器内部直接new VendorASdk(),或者用单例硬编码第三方对象。


✅ 通过构造函数注入(如上面的__construct(private VendorASdk $sdk)),测试时可以注入替身对象。



  1. ❌ 为了「解耦」给项目自己的每个类都套一层适配器。


✅ 适配器应该只出现在不可控的边界上(第三方 SDK、老系统接口、外部队列);内部类之间用接口直接协作即可。



  1. ❌ 认为适配器里应该顺手升级第三方库的调用方式。


✅ 适配器正是升级的隔离点:第三方换代时,改动被限制在适配器内部。但升级要连带更新翻译逻辑(方法名、错误码、字段名都可能变),并补上对应的测试。



  1. ❌ 用已经废弃或移除的扩展去实现底层能力(例如仍在用mcrypt_*做加解密)。


✅mcrypt_*从 PHP 7.2 起废弃、已不适合新项目;可逆加密改用sodium_*或带随机 nonce 的openssl_encrypt(),消息认证用hash_hmac()配合hash_equals()。适配器要适配的是接口,不是把过时实现换个包装继续用。


总结




关注点结论



解决什么让接口不兼容的两方协作,把第三方差异隔离在边界上

三个角色目标接口(自己定义)、适配者(第三方)、适配器(实现目标并委托)

首选形态对象适配器(持有 + 委托),PHP 单继承下类适配器基本不实用

与装饰器的区别装饰器保持接口不变并增强;适配器改变接口做转换

与工厂的关系工厂负责「创建哪个适配器」,适配器负责「怎么转换」

适配器的边界只出现在不可控的外部依赖处,内部类之间不需要



适配器的价值来自一句话:把「会变的东西」关进一个类里。第三方的 SDK 会升级、错误码会变、供应商会被替换,而业务代码只依赖自己定义的接口——这个接口就是那道墙,适配器则是墙上唯一的门。用它的时候要守住两条线:目标接口里不出现第三方类型,适配器里不写业务逻辑。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 16:14:40

Python爬虫实战:requests与XPath抓取图片站第一页

1. 图片站抓取任务的整体拆解与思路设计1.1 为什么选这个场景练手图片站抓取几乎是每个学 Python 爬虫的人都会碰到的第一个"有点意思"的实战项目。它比抓新闻列表直观——抓下来的东西肉眼可见&#xff0c;成不成立刻就知道&#xff1b;又比抓动态接口简单——大部分…

作者头像 李华
网站建设 2026/10/16 0:26:27

Windows 64位Oracle 12c客户端安装配置与TNS解析避坑指南

简介&#xff1a;Oracle 12c 客户端 Windows 64 位官方安装包&#xff0c;面向需要在 Windows 平台连接和管理 Oracle 数据库的 DBA、后端开发与运维人员。它提供 SQL*Plus、SQL Developer、Oracle Net Services 等核心组件&#xff0c;解决客户端与不同操作系统数据库服务器之…

作者头像 李华
网站建设 2026/10/20 1:33:02

Win7 64位打印机装不上?PrintSpooler服务排查与修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 20:49:04

编译产物如何落地?从本地运行到手机端部署的完整指南

编译成功之后&#xff0c;很多人会愣在原地。无论是用 GCC、MSVC 还是其他编译器&#xff0c;辛辛苦苦把源码撸成了可执行文件&#xff0c;结果下一个问题立刻冒出来&#xff1a;这东西到底怎么在本地跑起来&#xff1f;怎么塞进手机里用&#xff1f;网上教程往往只讲到“编译通…

作者头像 李华
网站建设 2026/10/16 5:18:57

Claude 拔网线后,Trae 用户的 AI 编程工作流还能怎么接 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华