单例模式
单例模式
意图
《设计模式》对单例模式意图的描述如下
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
从意图来看,单例模式的两个核心为「保证一个类仅有一个实例」以及 「提供一个访问它的全局访问点」。
为什么要保证一个类仅有一个实例?有时候是为了控制某些共享资源,例如一个系统仅有一个打印假脱机、文件系统和窗口管理器。
如何保证一个类仅有一个实例?首先,通过构造函数来获取实例肯定是不行的,因为构造函数每次都会生成新的实例对象。其次,单单提供一个全局变量来访问实例也不够,因为不能保证获取的是唯一实例。因此,更好的解决方式就是让类自身来负责保存它的唯一实例,并提供一个访问该实例的方法。这样的话,对于客户端而言,每次获取的都是同一个实例。不过,类既负责了实例的保存,也负责了实例的访问,违反了 单一职责 原则。
实现
单例模式的实现可以分为两种,在类中直接实现单例模式,或者通过继承的方式来实现单例模式。
在类中实现单例模式
第一种方法就是我们在类中直接实现单例模式,这也是最常用的方法
<?php
final class Singleton
{
// 保存实例
private static $instance;
// 第一次获取时创建
public static function getInstance(): Singleton
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
// 防止通过构造函数实例化
private function __construct(){}
// 防止被克隆
private function __clone(){}
// 防止被反系列化
private function __wakeup(){}
}
通过继承实现单例模式
另外一种方式就是定义一个全局的单例类,子类只需要继承该类就可以实现单例模式。
<?php
class Singleton
{
// 保存一个或多个实例
private static $instances = [];
protected function __construct(){}
protected function __clone(){}
public function __wakeup()
{
throw new \Exception("不能反系列化单例");
}
public static function getInstance(): Singleton
{
$subclass = static::class;
if (!isset(self::$instances[$subclass])) {
self::$instances[$subclass] = new static();
}
return self::$instances[$subclass];
}
}
// 测试
class Foo extends Singleton
{
}
Foo::getInstance() === Foo::getInstance(); // true
应用
为了更加方便的管理全局变量,可使用单例模式来管理应用配置。但是当我们使用单例模式控制全局变量时,也导致全局变量存在被覆盖的风险。
<?php
class Config extends Singleton
{
private $data = [];
public function getValue(string $key): string
{
return $this->data[$key];
}
public function setValue(string $key, string $value): void
{
$this->data[$key] = $value;
}
}
$config1 = Config::getInstance();
$config1->setValue("name", "foo");
$config2 = Config::getInstance();
$config2->getValue("name") === "foo"; // true
对于客户端而言,基于文件的日志管理实例有且仅有一个,可防止文件写入冲突,可使用单例模式来实现。
<?php
class Logger extends Singleton
{
private $fileHandle;
protected function __construct()
{
// 简化代码,使用标准输出,实际应用中可使用文件
$this->fileHandle = fopen('php://stdout', 'w');
}
// 写入日志
public function writeLog(string $message): void
{
$date = date('Y-m-d');
fwrite($this->fileHandle, "$date: $message\n");
}
// 提供快捷的日志记录方法
public static function log(string $message): void
{
$logger = static::getInstance();
$logger->writeLog($message);
}
}
Logger::log("日志1");
Logger::log("日志2");
总结
当你要控制某些共享资源时,可使用单例模式。但是单例模式也存在几点不足
- 单例模式同时负责类的保存与访问,违反了「单一职责」原则,因此,单例模式被认为是反模式。
- 在多线程环境中,需要对单例模式进行特殊处理,防止多个线程同时创建单例对象。
- 由于单例模式不能被实例化,将导致单元测试难以进行。