直接 new Logger() 会导致日志切换困难,因其硬编码具体实现,使业务逻辑与写入方式强耦合;桥接模式通过分离「谁来记」(Logger抽象)和「怎么记」(LogWriter实现)两个维度,实现运行时灵活切换。

为什么直接 new Logger() 会导致日志切换困难
硬编码日志实现(比如 new FileLogger() 或 new DatabaseLogger())会让业务逻辑和具体写入方式强耦合。一旦要从文件切到 Redis,或增加 SaaS 日志服务,就得改所有调用点——这不是切换,是重写。
桥接模式的核心不是“多写几个类”,而是把「谁来记」(抽象:Logger)和「怎么记」(实现:Writer)拆成两个可独立变化的维度。
-
Logger类只关心「log($level, $message)」这类接口,不碰路径、连接、序列化 -
LogWriter接口定义「write($content)」,所有具体实现(FileWriter、StdoutWriter、RedisWriter)只管自己那一套写法 - 运行时通过构造函数注入 Writer 实例,切换只需换一个对象,不改任何业务代码
如何用 PHP 实现可插拔的 LogWriter 接口
关键在定义稳定、窄小的接口。别让 LogWriter 承担格式化、分级、异步等职责——那些归 Logger 管;它只干一件事:把一串字符串落下去。
interface LogWriter
{
public function write(string $content): void;
}
每个实现类专注自身协议:
-
FileWriter处理fopen()、flock()、按日期滚动 -
RedisWriter只管$redis->rPush('logs', $content),不关心是否压缩或分片 -
NullWriter用于测试或禁用日志,public function write(string $content): void {}
注意:write() 方法必须是同步阻塞的,否则桥接层无法可靠感知失败(比如磁盘满、Redis断连),错误应由具体 Writer 抛出,由上层统一捕获。
Logger 抽象类如何桥接并支持运行时切换
Logger 不继承任何具体 Writer,而是持有 LogWriter 接口实例,并提供 setWriter() 方法。这比构造时固定更灵活——比如根据请求 header 动态切 writer,或降级时临时切到 StdoutWriter。
class Logger
{
private LogWriter $writer;
public function __construct(LogWriter $writer)
{
$this->writer = $writer;
}
public function setWriter(LogWriter $writer): void
{
$this->writer = $writer;
}
public function info(string $message): void
{
$this->writer->write(date('c') . ' [INFO] ' . $message);
}
}
常见误操作:
- 在
setWriter()里做instanceof判断并调用私有方法——这破坏了桥接的解耦意义 - 让
Logger实现LogWriter——抽象类不该同时当桥和桥墩 - 把格式化逻辑(如 JSON 封装)写进
write()调用前——应由Logger子类或装饰器处理,保持 Writer 纯净
生产环境切换时最容易被忽略的三点
动态切换不是调个 setWriter() 就完事。真实场景中,这些细节决定是否真能热切换:
- Writer 实例本身需是线程/请求安全的:比如
RedisWriter持有的$redis连接不能被多个请求共用导致状态错乱,应每次新建或用连接池管理 - 旧 Writer 的资源清理:切换前是否要 flush 缓存?
FileWriter是否要fclose()?这得在setWriter()内部处理,而不是靠用户记得手动 close - 切换时机:不要在中间件或控制器里随意调
setWriter(),最好限定在应用生命周期早期(如容器绑定时)或明确的降级开关处,避免因并发导致部分日志写旧路径、部分写新路径
桥接的价值不在结构漂亮,而在 Writer 替换时,你不需要 grep 全项目找 new FileLogger ——只要确保 Logger 实例是单例或容器托管,切换就只是换一个依赖。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











