在 PHP 面试中,设计模式相关的问题几乎不可避免。其中,单例模式、工厂模式和依赖注入是最常被问到的三个概念。很多候选人对它们的定义倒背如流,但一旦被追问“实际项目中什么时候用、为什么用”,就容易语塞。本文将从面试实战角度出发,结合 PHP 代码示例,帮你彻底理清这三者的使用场景和区别。
一、单例模式(Singleton Pattern)
1.1 核心思想
单例模式确保一个类只有一个实例,并提供一个全局访问点。在 PHP 中,由于每次请求结束后所有对象都会被销毁,单例模式的价值主要体现在单次请求生命周期内避免重复创建重量级对象。
1.2 典型 PHP 实现
class Database
{
private static ?Database $instance = null;
private \PDO $pdo;
private function __construct()
{
$this->pdo = new \PDO('mysql:host=localhost;dbname=test', 'root', '');
}
public static function getInstance(): Database
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
public function getConnection(): \PDO
{
return $this->pdo;
}
}
1.3 使用场景
- 数据库连接:一个请求中多次操作数据库,复用同一个 PDO 连接,避免反复建立连接的开销。
- 配置管理器:全局配置对象,整个请求周期内只需加载一次。
- 日志记录器:多个模块共享同一个日志实例,保证日志写入顺序一致。
- 缓存管理器:如 Redis 连接实例,避免重复连接。
1.4 面试常见追问
问:单例模式有什么缺点?
答:单例本质上是一种全局状态,会导致代码耦合度高、难以单元测试(因为无法轻松替换 mock 对象)、隐藏依赖关系。在现代 PHP 框架(如 Laravel、Symfony)中,更推荐用依赖注入容器来管理共享实例,而不是手写单例。
问:PHP 中单例是线程安全的吗?
答:PHP 本身是单线程模型(每个请求独立进程),不存在传统多线程竞争问题。但在 Swoole 等协程环境下,单例需要额外注意协程安全问题。
二、工厂模式(Factory Pattern)
2.1 核心思想
工厂模式将对象的创建过程封装起来,调用方只需告诉工厂“我要什么”,而不关心“怎么造”。它分为简单工厂、工厂方法和抽象工厂三种变体。
2.2 典型 PHP 实现
interface PaymentInterface
{
public function pay(float $amount): string;
}
class Alipay implements PaymentInterface
{
public function pay(float $amount): string
{
return "支付宝支付 {$amount} 元";
}
}
class WechatPay implements PaymentInterface
{
public function pay(float $amount): string
{
return "微信支付 {$amount} 元";
}
}
class PaymentFactory
{
public static function create(string $type): PaymentInterface
{
return match ($type) {
'alipay' => new Alipay(),
'wechat' => new WechatPay(),
default => throw new \InvalidArgumentException("不支持的支付方式: {$type}"),
};
}
}
// 使用
$payment = PaymentFactory::create('alipay');
echo $payment->pay(100.00);
2.3 使用场景
- 支付网关:根据用户选择动态创建支付宝、微信、银联等支付对象。
- 日志驱动:根据配置创建文件日志、数据库日志或 Elasticsearch 日志处理器。
- 缓存驱动:根据环境切换 Redis、Memcached、文件缓存。
- ORM 连接:Laravel 的
DB::connection('mysql')底层就是工厂模式的体现。
2.4 面试常见追问
问:工厂模式和简单 new 对象有什么区别?
答:直接 new 会把具体类名硬编码在业务代码中,一旦类名变更或需要增加初始化逻辑,就要修改多处。工厂模式将创建逻辑集中到一处,符合开闭原则——新增支付方式时只需扩展工厂,不需改动调用方。
问:工厂模式和依赖注入冲突吗?
答:不冲突。工厂本身可以通过依赖注入获取,而工厂创建的对象也可以被注入到其他服务中。在 Laravel 中,app()->make() 本质上就是一个高级工厂 + 容器的组合。
三、依赖注入(Dependency Injection)
3.1 核心思想
依赖注入是指由外部将依赖传递给对象,而不是对象自己创建依赖。它是控制反转(IoC)的一种实现方式,目的是解耦。
3.2 典型 PHP 实现
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function log(string $message): void
{
file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
}
}
class UserService
{
public function __construct(
private LoggerInterface $logger
) {}
public function register(string $name): void
{
// 注册逻辑...
$this->logger->log("用户 {$name} 已注册");
}
}
// 手动注入
$service = new UserService(new FileLogger());
$service->register('张三');
3.3 使用场景
- 控制器依赖服务层:Laravel 控制器构造函数中直接类型提示
UserService,容器自动注入。 - 测试替换 Mock:单元测试时注入一个
MockLogger,无需修改业务代码。 - 多环境切换实现:开发环境注入
FileLogger,生产环境注入ElasticLogger。 - AOP 与中间件:通过容器管理对象生命周期,方便在注入前后添加切面逻辑。
3.4 面试常见追问
问:依赖注入和依赖注入容器是一回事吗?
答:不是。依赖注入是一种设计原则,手动 new 并传参也算依赖注入。依赖注入容器是一个工具,用于自动解析和注入依赖,比如 Laravel 的 app() 和 Symfony 的 Container。
问:依赖注入相比单例有什么优势?
答:依赖注入让依赖关系显式化,便于测试和替换;单例隐藏依赖且难以 mock。在现代框架中,容器可以通过绑定单例来模拟单例行为,同时保留可测试性:
// Laravel 中绑定单例
app()->singleton(LoggerInterface::class, FileLogger::class);
四、三者的关系与选择建议
| 维度 | 单例模式 | 工厂模式 | 依赖注入 |
|---|---|---|---|
| 解决的问题 | 控制实例数量 | 封装对象创建 | 解耦依赖关系 |
| 关注点 | 全局唯一性 | 创建逻辑集中 | 谁提供依赖 |
| 测试友好度 | 低 | 中 | 高 |
| 现代框架中的替代 | 容器 singleton 绑定 | 容器 make / 工厂类 | 容器自动注入 |
实战选择建议:
- 如果只是想让某个服务在请求内共享,优先用依赖注入容器的 singleton 绑定,而不是手写单例。
- 如果对象创建逻辑复杂(如根据配置、环境、参数决定实例化哪个类),用工厂模式。
- 如果目标是解耦和可测试,依赖注入是首选,工厂和单例都可以作为它的补充。
五、面试答题模板
当面试官问“这三个模式的使用场景”时,可以这样组织回答:
单例模式适合管理数据库连接、配置、日志等全局唯一且创建成本高的资源,但要注意它带来的耦合和测试困难。工厂模式适合对象创建逻辑复杂或需要根据条件动态选择实现的场景,比如支付网关、缓存驱动。依赖注入则是现代 PHP 开发的核心实践,通过构造函数或容器注入依赖,实现解耦和可测试性。在实际项目中,我通常用容器管理单例,用工厂封装复杂创建逻辑,用依赖注入贯穿业务代码。
这样的回答既有理论深度,又体现了工程实践经验,能给面试官留下良好印象。
未经允许不得转载:任鹏个人博客 » PHP 面试题:PHP 中单例模式、工厂模式和依赖注入的使用场景

