桥接模式的核心是抽象与实现分离,抽象类必须依赖实现接口而非具体类,接口应窄而稳定,运行时通过setter或重建实例切换实现,目录和命名需体现正交维度。

桥接模式不是“怎么配个开关就完事”的小技巧,它解决的是两个维度必须独立变化、又不能靠继承爆炸式膨胀的硬需求。用错地方反而增加复杂度,用对了才能让 Service 换 Formatter 像换电池一样自然。
抽象层必须持有实现接口引用,不能直接依赖具体类
这是桥接最核心的约束:抽象类(比如 Service)的构造函数参数类型必须是接口(如 Formatter),而不是某个具体实现(如 HtmlFormatter)。否则运行时无法替换实现,桥接就退化成普通继承。
- 错误写法:
public function __construct(HtmlFormatter $printer)—— 锁死 HTML 渲染,后续加JsonFormatter就得改构造签名,破坏开闭原则 - 正确写法:
public function __construct(Formatter $implementation)—— 只认接口契约,PlainTextFormatter、HtmlFormatter都能传进来 - 注意:PHP 8.0+ 支持联合类型,但桥接场景下仍应坚持单一接口类型,避免把
Formatter|Logger这类混搭塞进同一个参数
实现接口要窄而稳定,不暴露无关方法
Formatter 接口只该声明 format(string $text): string 这一个方法。如果加了 setEncoding() 或 isSupported(),就会让所有实现类被迫实现它们,哪怕 PlainText 根本不需要编码处理。
- 接口膨胀的后果:新增
PdfFormatter时,不得不为不相关的编码逻辑打桩或抛RuntimeException - 真实场景中常见错误:把日志级别判断、缓存开关等业务逻辑塞进
Renderer::render(),导致HtmlRenderer和JsonRenderer被迫耦合非渲染职责 - 验证方式:删掉接口里任意一个方法,看是否仍有具体类因此报错 —— 如果有,说明那个方法本不该存在
运行时切换实现必须通过 setter 或重建实例,不能靠 if-else 分支
桥接的价值在于运行时动态解耦。如果在 Service::get() 里写 if ($this->type === 'html') { ... },那只是把条件逻辑从客户端挪到了抽象层内部,桥接结构形同虚设。
- 推荐做法:提供
setImplementation(Formatter $printer)方法,允许外部随时注入新实现 - 替代方案:不提供 setter,而是让客户端重新 new
Service($newFormatter)—— 更显式,也更利于 DI 容器管理 - 反模式示例:
private $type = 'html';+match($this->type) { 'html' => new HtmlFormatter(), ... }—— 实现选择被固化在抽象类内部,违背桥接初衷
命名和目录结构要反映“抽象-实现”分离意图
文件组织比代码逻辑更容易暴露设计意图。把 Service.php 和 HtmlFormatter.php 放进同一级目录,或者叫 HtmlService.php,都会让人误以为它们是强绑定关系。
- 建议目录结构:
src/Abstraction/Service.php+src/Implementation/Formatter/HtmlFormatter.php - 类名避免交叉污染:
ColorfulCircle是错的,Circle(抽象) +Red(实现)才是桥接风格 - PSR-4 自动加载需匹配命名空间,例如
App\Abstraction\Service和App\Implementation\Formatter\HtmlFormatter,否则new Service(new HtmlFormatter())会因找不到类而失败
真正难的不是写出能跑通的桥接代码,而是判断当前业务是否存在两个正交变化维度——比如「通知渠道」(邮件 / 短信 / 钉钉)和「通知内容模板」(入职提醒 / 考勤异常 / 年度总结);一旦其中一个维度未来可能新增 5 种以上变体,桥接就不再是可选项,而是避免类爆炸的必要设计。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











