PHP - 单例模式 - 代码分析与良好实践
这段代码实现了一个支持多态的单例模式(Multiton Pattern),具有以下特点: 使用trait封装单例逻辑,可复用 通过静态变量$instance数组存储不同类的单例实例 支持可变参数构造(...$arg) 采用后期静态绑定(static::class)实现多态,每个子类拥有自己的单例实例 包含实例存在性和类型检查 典型应用场景包括配置管理、数据库连接等需要全局唯一实例的场景。该实现简洁
参考:
操作系统 - 线程安全 - 学习_操作系统课设线程安全型-CSDN博客
PHP - 版本选择 - 线程安全与非线程安全 - 学习/实践_php线程安全和非线程安全-CSDN博客
PHP - 静态方法与非静态方法的区别[含性能,内存消耗] - 学习/实践_非静态方法节省内存吗-CSDN博客
设计模式 - 单例模式与工厂模式_php单例模式和工厂模式-CSDN博客
<?php
namespace App\Services\Traits;
trait SingletonTrait
{
private static $instance = [];
public static function getInstance(...$arg)
{
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self)) {
self::$instance[static::class] = new static(...$arg);
}
return self::$instance[static::class];
}
}
分析上面的一段代码(同事的编码)
分析如下:
单例模式实现代码解析
这段代码实现了一个多态单例模式(Multiton pattern),即允许每个子类拥有自己的单例实例。下面我将详细解释这段代码的各个部分:
代码功能概述
public static function getInstance(...$arg)
{
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self)) {
self::$instance[static::class] = new static(...$arg);
}
return self::$instance[static::class];
}
这是一个静态方法,用于获取当前类的单例实例,具有以下特点:
- 支持多态(子类有自己的单例实例)
- 支持可变参数构造
- 线程不安全(PHP本身是单线程的,所以不是问题)
逐行解析
1. 方法签名
public static function getInstance(...$arg)
static
:静态方法,通过类名直接调用
...$arg
:可变参数,允许传入任意数量的参数给构造函数
2. 条件判断
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self))
两个条件满足其一就会创建新实例:
!isset(self::$instance[static::class])
:当前类的实例不存在!(self::$instance[static::class] instanceof self)
:存储的实例不是当前类或其子类的实例
3. 创建新实例
self::$instance[static::class] = new static(...$arg);
new static(...$arg)
:后期静态绑定,创建当前调用类的实例
...$arg
:将传入的参数展开传递给构造函数
4. 返回实例
return self::$instance[static::class];
返回当前类的单例实例
关键概念解释
1. static::class
vs self::class
static::class
:返回实际调用的类名(后期静态绑定)
self::class
:返回定义该代码的类名
示例:
class ParentClass {
public static function who() {
echo static::class;
}
}
class ChildClass extends ParentClass {}
ChildClass::who(); // 输出 "ChildClass"
2. 多态单例模式
这个实现允许:
class A {
use SingletonTrait;
}
class B extends A {}
$a1 = A::getInstance(); // A的实例
$a2 = A::getInstance(); // 同上实例
$b1 = B::getInstance(); // B的实例(不同于A的实例)
3. 可变参数 ...$arg
允许灵活地传递构造函数参数:
class Config {
public function __construct($path, $default = []) {}
}
$config = Config::getInstance('/path/to/config', ['key' => 'value']);
完整实现示例
通常这种单例模式会配合静态属性使用:
trait SingletonTrait {
private static $instances = [];
public static function getInstance(...$args) {
if (!isset(self::$instances[static::class]) ||
!(self::$instances[static::class] instanceof static)) {
self::$instances[static::class] = new static(...$args);
}
return self::$instances[static::class];
}
// 防止外部实例化
private function __construct() {}
private function __clone() {}
private function __wakeup() {}
}
class Database {
use SingletonTrait;
private $connection;
protected function __construct($host, $user, $pass) {
$this->connection = new PDO("mysql:host=$host", $user, $pass);
}
}
$db = Database::getInstance('localhost', 'root', 'password');
使用场景
- 配置管理:全局唯一的配置实例
- 数据库连接:避免重复创建连接
- 日志系统:统一的日志记录器
- 缓存系统:全局缓存管理
注意事项
- 不是线程安全:PHP本身是单线程的,但在PHP-FPM等多进程环境下仍需注意
- 测试困难:单例在单元测试中难以mock
- 生命周期:在长时间运行的进程(如Swoole)中可能导致内存泄漏
- 继承问题:子类化单例需要谨慎设计
替代方案
对于现代PHP开发,可以考虑:
- 依赖注入容器:
$container = new Container();
$container->singleton(Database::class, function() {
return new Database();
});
- 静态工厂:
class Factory {
private static $dbs = [];
public static function getDb($name) {
if (!isset(self::$dbs[$name])) {
self::$dbs[$name] = new Database();
}
return self::$dbs[$name];
}
}
这段代码展示了PHP中灵活运用静态方法和后期静态绑定实现多态单例模式的经典方式,是很多框架和库中常见的设计模式实现。
下面代码的区别
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self))
//与
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof static))
//与
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof self))
//的区别
PHP 中静态属性访问与实例检查的区别
这几种写法在 PHP 中有重要区别,主要涉及静态绑定和实例检查的行为差异。下面我将详细分析每种写法的含义和适用场景。
四种写法的对比分析
1. 原始写法
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self))
self::$instance
:访问定义该代码的类中的静态属性$instance
instanceof self
:检查是否为定义该代码的类的实例
- 特点:静态属性访问和实例检查都使用早期静态绑定
2. 变体1
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof static))
static::$instance
:后期静态绑定访问静态属性
instanceof static
:检查是否为实际调用类的实例
- 特点:完全的后期静态绑定,最符合多态单例的需求
3. 变体2
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof self))
static::$instance
:后期静态绑定访问静态属性
instanceof self
:检查是否为定义该代码的类的实例
- 特点:混合模式,属性访问是多态的,但实例检查是静态的
详细区别
静态属性访问的区别
写法 |
行为 |
继承时的表现 |
|
总是访问定义该属性的类 |
子类无法覆盖静态属性 |
|
动态解析访问当前类 |
子类可以有自己的静态属性 |
实例检查的区别
写法 |
行为 |
继承时的表现 |
|
检查是否为定义类的实例 |
严格检查,不接受子类实例 |
|
检查是否为当前类的实例 |
多态检查,接受子类实例 |
实际效果演示
class ParentClass {
protected static $instance = [];
public static function getInstance() {
// 测试四种不同写法
var_dump([
'self::$instance' => isset(self::$instance[static::class]),
'static::$instance' => isset(static::$instance[static::class]),
'instanceof self' => self::$instance[static::class] instanceof self,
'instanceof static' => self::$instance[static::class] instanceof static
]);
}
}
class ChildClass extends ParentClass {
protected static $instance = [];
}
ChildClass::getInstance();
最佳实践建议
- 多态单例模式:使用完全后期静态绑定的版本
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof static))
- 严格单例模式:如果希望所有子类共享同一个实例
if (!isset(self::$instance[static::class]) || !(self::$instance[static::class] instanceof self))
- 避免混合模式:
static::$instance + instanceof self
通常不是好的设计
性能考虑
static::
比self::
有轻微性能开销(动态解析)
- 在单例模式中这种差异可以忽略不计
- 高频调用场景才需要考虑优化
完整示例
trait SingletonTrait {
protected static $instances = [];
public static function getInstance(...$args) {
// 最佳实践:完全后期静态绑定
if (!isset(static::$instances[static::class]) ||
!(static::$instances[static::class] instanceof static)) {
static::$instances[static::class] = new static(...$args);
}
return static::$instances[static::class];
}
}
class Logger {
use SingletonTrait;
protected static $instances = []; // 每个类有自己的静态属性
private function __construct() {}
}
class FileLogger extends Logger {
protected static $instances = []; // 覆盖父类的静态属性
}
总结
self::
是早期静态绑定,编译时确定static::
是后期静态绑定,运行时确定instanceof self
检查定义类的实例instanceof static
检查当前调用类的实例
在单例模式实现中,通常希望子类有自己的单例实例,因此推荐使用完全后期静态绑定的版本:
if (!isset(static::$instance[static::class]) || !(static::$instance[static::class] instanceof static))
扩展:
PHP - 静态方法与非静态方法的区别[含性能,内存消耗] - 学习/实践
PHP 自己实现一个单例模式 给出完整的代码
PHP 单例模式完整实现
下面我将给出 PHP 中单例模式的完整实现,包含多种变体和最佳实践。
基础单例模式实现
class Singleton
{
/**
* @var Singleton 保存类实例的静态变量
*/
private static $instance;
/**
* 私有构造函数,防止外部实例化
*/
private function __construct()
{
// 初始化代码
}
/**
* 私有克隆方法,防止克隆实例
*/
private function __clone()
{
}
/**
* 私有反序列化方法,防止反序列化创建新实例
*/
private function __wakeup()
{
}
/**
* 获取单例实例
*
* @return Singleton
*/
public static function getInstance(): Singleton
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
/**
* 示例方法
*/
public function doSomething(): void
{
echo "Singleton is working...\n";
}
}
// 使用示例
$singleton = Singleton::getInstance();
$singleton->doSomething();
// 测试单例
$anotherInstance = Singleton::getInstance();
var_dump($singleton === $anotherInstance); // 输出: bool(true)
支持继承的多态单例模式
abstract class Multiton
{
/**
* @var array 保存各个子类实例
*/
private static $instances = [];
/**
* 私有构造函数
*/
private function __construct()
{
}
/**
* 获取当前类的单例实例
*
* @return static
*/
public static function getInstance()
{
$className = static::class;
if (!isset(self::$instances[$className])) {
self::$instances[$className] = new static();
}
return self::$instances[$className];
}
/**
* 防止克隆
*/
private function __clone()
{
}
/**
* 防止反序列化
*/
private function __wakeup()
{
}
}
class Logger extends Multiton
{
public function log(string $message): void
{
echo "Logging: $message\n";
}
}
class Config extends Multiton
{
private $settings = [];
public function set(string $key, $value): void
{
$this->settings[$key] = $value;
}
public function get(string $key)
{
return $this->settings[$key] ?? null;
}
}
// 使用示例
$logger1 = Logger::getInstance();
$logger2 = Logger::getInstance();
var_dump($logger1 === $logger2); // true
$config = Config::getInstance();
$config->set('debug', true);
线程安全的单例模式(适用于PHP-FPM等环境)
class ThreadSafeSingleton
{
private static $instance;
private static $lock = false;
private function __construct()
{
// 初始化代码
}
public static function getInstance(): ThreadSafeSingleton
{
if (null === static::$instance && !static::$lock) {
static::$lock = true;
try {
static::$instance = new static();
} finally {
static::$lock = false;
}
}
return static::$instance;
}
private function __clone()
{
}
private function __wakeup()
{
}
}
支持依赖注入的单例模式
class DIContainer
{
private static $instance;
private $services = [];
private function __construct()
{
}
public static function getInstance(): DIContainer
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
public function register(string $name, callable $resolver): void
{
$this->services[$name] = $resolver;
}
public function resolve(string $name)
{
if (!isset($this->services[$name])) {
throw new \RuntimeException("Service $name not found");
}
return call_user_func($this->services[$name]);
}
}
// 使用示例
$container = DIContainer::getInstance();
$container->register('db', function() {
return new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
});
$db = $container->resolve('db');
单例模式最佳实践
- 避免全局状态:单例本质上是全局状态,应谨慎使用
- 考虑依赖注入:优先考虑依赖注入而非直接使用单例
- 测试友好设计:
class Singleton
{
private static $instance;
private static $testingInstance = null;
public static function setTestingInstance($instance): void
{
self::$testingInstance = $instance;
}
public static function getInstance()
{
if (self::$testingInstance !== null) {
return self::$testingInstance;
}
if (null === self::$instance) {
self::$instance = new static();
}
return self::$instance;
}
}
- PSR-11 容器:考虑使用
psr/container
实现而非自制单例
单例模式在框架中的应用
Laravel 中的单例绑定
// 服务容器中注册单例
app()->singleton('cache', function() {
return new CacheManager();
});
// 使用
$cache = app('cache');
Symfony 中的单例服务
# config/services.yaml
services:
App\Service\SingletonService:
shared: true # 单例模式
总结
单例模式的核心要点:
- 私有构造函数防止外部实例化
- 静态方法获取唯一实例
- 防止克隆和反序列化
- 根据需求选择基础单例或多态单例
- 在现代化PHP开发中,通常使用依赖注入容器替代原生单例模式
PHP 单例模式核心要点详解
下面我将详细解释单例模式的五个核心要点及其背后的设计原理:
1. 私有构造函数防止外部实例化
实现方式:
private function __construct()
{
// 初始化代码
}
原因:
- 控制实例化过程:确保只能通过指定的静态方法创建实例
- 避免随意创建:防止其他开发人员直接使用
new
关键字创建多个实例
- 强制使用单例接口:必须通过
getInstance()
方法获取实例
- 设计意图明确:通过语法限制明确表达这是单例类
反例说明: 如果构造函数是公开的:
$instance1 = new Singleton();
$instance2 = new Singleton();
// $instance1 !== $instance2,违背单例原则
2. 静态方法获取唯一实例
实现方式:
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
原因:
- 全局访问点:提供统一的实例获取途径
- 延迟初始化:首次访问时才创建实例(节省资源)
- 实例控制:在方法内实现实例存在性检查
- 线程安全控制点:可以在该方法内添加同步锁(虽然PHP通常不需要)
关键特性:
$singleton1 = Singleton::getInstance();
$singleton2 = Singleton::getInstance();
// $singleton1 === $singleton2 保证唯一性
3. 防止克隆和反序列化
实现方式:
private function __clone()
{
// 防止克隆
}
private function __wakeup()
{
// 防止反序列化
}
原因:
- 防止克隆:
clone
关键字会创建对象副本- 私有化
__clone()
方法会在尝试克隆时抛出错误
- 防止反序列化:
- 序列化/反序列化会创建新对象
- PHP 的
unserialize()
会调用__wakeup()
- 私有化
__wakeup()
可防止通过这种方式创建新实例
漏洞演示: 如果没有这些防护:
$singleton = Singleton::getInstance();
$clone = clone $singleton; // 创建了副本
$serialized = serialize($singleton);
$another = unserialize($serialized); // 又创建了新实例
4. 基础单例 vs 多态单例
基础单例
class Singleton {
private static $instance;
public static function getInstance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
}
多态单例(支持继承)
class Multiton {
private static $instances = [];
public static function getInstance() {
$cls = static::class; // 后期静态绑定
if (!isset(self::$instances[$cls])) {
self::$instances[$cls] = new static();
}
return self::$instances[$cls];
}
}
class Child extends Multiton {}
选择依据:
- 基础单例:当确定不需要继承时使用,更简单直接
- 多态单例:当需要单例行为可继承时使用
- 每个子类有自己的单例实例
- 适用于框架基础类等场景
5. 依赖注入容器替代原生单例
现代PHP框架的做法:
// Laravel 的服务容器示例
app()->singleton('cache', function() {
return new RedisCache();
});
// 使用
$cache = app('cache'); // 总是返回同一个实例
为什么推荐依赖注入容器:
- 更灵活的 lifecycle 控制:
不只是单例,还可以控制为:
- 每次解析新实例(绑定为普通方式)
- 请求生命周期单例(Laravel 的
scoped
) - 上下文相关实例
- 更好的可测试性:
// 测试时可以替换实现
$mockCache = Mockery::mock(CacheInterface::class);
app()->instance('cache', $mockCache);
- 解耦依赖关系:
- 不直接依赖具体单例类
- 通过接口和容器解析依赖
- 配置集中管理:
// 在服务提供者中配置
public function register()
{
$this->app->singleton(Database::class, function() {
return new Database(config('database'));
});
}
- 支持自动注入:
class UserController {
public function __construct(protected Database $db) {}
// 容器会自动注入已注册的单例
}
传统单例模式的问题:
- 硬编码依赖:
Database::getInstance()
使类与具体实现紧密耦合
- 难以模拟:在测试中难以替换单例实例
- 全局状态:使应用状态难以追踪和管理
- 缺乏灵活性:无法根据需要改变生命周期
总结对比
特性 |
传统单例 |
依赖注入容器 |
实例控制 |
类自身控制 |
容器集中控制 |
可测试性 |
差 |
好 |
灵活性 |
低 |
高 |
生命周期 |
仅单例 |
多种生命周期 |
耦合度 |
高耦合 |
低耦合 |
适用场景 |
简单应用 |
中大型应用 |
最终建议:
- 小型工具类或确定不需要扩展的场合:使用传统单例
- 应用核心组件或可能扩展的类:使用依赖注入容器
- 框架开发:优先考虑容器绑定方式
更多推荐
所有评论(0)