单例模式 (singleton)
手握设计模式宝典 - 单例模式 singleton
Design-Pattern - Creational - Singleton
- Title: 《手握设计模式宝典》 之 单例模式
singleton
- Tag:
Design-Pattern
、Creational
、Singleton
、单例模式
、设计模式
- Author: Tacks
- Create-Date: 2023-08-03
- Update-Date: 2023-08-10
0、REF
- refactoringguru
- designpatternsphp - 单例模式
- PHP 设计模式全集 - 单例模式(Singleton)
- PHP 设计模式学习 - 单例模式(Singleton)
- 设计模式实践–单例模式
- 极简设计模式-单例模式
- 我想在父类中实现单例模式,结果却踩了一个坑!
1、5W1H
1.1 WHAT 什么是单例模式?
1.1.1 概念
单例模式 是一种 创建型 设计模式,能够保证一个类只有一个实例,提供一个全局的访问点来获取该实例。
单例模式通常是通过构造函数私有,防止通过直接 new
来获取对象的实例,而是通过静态方法来获取对象的单例。
1.1.2 真实世界类比
通常来说一个部门或者项目会有一个 OWNER
,如果其他部门想要首次进行沟通或者对接,必须通过部门 OWNER
来获取实例,从而得到接下来沟通的必要信息。比如 A 部门的员工 X,想要 B 部门的 y 的一些项目资料,那么必须由 A 部门的 OWNER
跟 B 部门的 OWNER
有一个初步的沟通,再放权下来让 x 和 y 对接。
TIPS: 部门
OWNER
的设计可以确保对接流程有一个中心化的控制点,也就是充当了 单例模式 中全局访问点的角色。 通过部门OWNER
作为获取实例的唯一途径,可以更好管理部门之间的合作,从而提高工作效率。
1.2 WHY 为什么有单例模式?
1.2.1 解决了什么问题
- 保证一个类全局只有一个实例 :避免多次创建相同对象的开销以及资源的浪费;
- 提供一个全局访问点 :应用程序任何地方的代码都可以方便的访问同一个实例;
1.2.2 带来了什么问题
- 引起全局状态变化 :由于单例模式的实例是全局唯一的,任何对该实例的修改都会影响到整个应用程序
- 违反单一职责原则 :同时负责创建实例和获取实例(反模式)
- 多线程中注意避免 :同时多个线程调用得到了不同的对象(php可以不用考虑,可以用 go 模拟一下,需要加锁
sync.Mutex
或者sync.Once
) - 单元测试难以处理 :因为单例模式创建的对象是全局唯一的,无法通过 mock 或者其他方式来替换
- 代码耦合度会增加 :单例的对象通常会被多个模块依赖
1.2.3 有什么更好方法
- 考虑使用 IOC 容器或者依赖注入 的方式来管理单例对象的生命周期和依赖关系 => 用来解决单例模式掌控自己的生命周期
1.3 WHERE 在什么地方使用单例模式?
- 数据库连接:数据库连接是一个耗时的操作,采用单例可以复用同一个连接,减少资源消耗
- 日志记录器:
- …
1.4 WHEN 什么时间使用?
- 当应用程序需要全局共享一个实例的时候
- 当你想控制实例的数量,节省资源
1.5 WHO 谁负责调用?
- 任意位置:想在哪里就在哪用,通常是
className::getInstance()
- 容器管控:通过 Container 容器服务器提供者中注册单例对象,通过 bindings 数组存储应用中需要用到的单例对象
1.6 HOW 如何去实现?
- 构造函数 私有 ,防止外部直接通过构造函数实例化
- 公共的静态方法作为构造函数,然后进行实例化,并且保存在 私有 的 静态属性中
- 克隆魔术方法
__clone()
私有 - 序列化魔术方法
__wakeup()
私有
2、Code
2.1 常规单例
namespace App\Creational\Singleton;
use Exception;
final class Singleton
{
// 唯一的实例
private static $instance = null;
/**
* 懒汉方式创建实例, 首次创建使用
*/
public static function getInstance() :Singleton
{
if(null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function handle()
{
$pid = getmypid();
echo sprintf("[cli-%s][%s] %s %s \n", $pid, date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__);
}
/**
* 构造函数需要是 private 访问权限的,避免外部通过 new 创建实例;
* 要想使用实例,必须从 Singleton::getInstance() 获取
*/
private function __construct()
{
}
/**
* 防止克隆函数,这将又会创造一个实例
*/
public function __clone()
{
throw new Exception("Singleton can not clone");
}
/**
* 防止序列化
*/
public function __wakeup()
{
throw new Exception("Singleton can not unserialize");
}
}
2.2 单例基类
- 提供全局单例的基类
SingletonBase
允许其他类继承就可以使用单例模式 - 私有静态属性,提供所有子类唯一实例化的对象数组
private static $instances
namespace App\Creational\Singleton;
use Exception;
/**
* 单例基类 SingletonBase
*/
class SingletonBase
{
/**
* 实例化的单例,都保存在静态属性中
*
* @var array
*/
private static $instances = [];
/**
* 懒汉方式创建实例, 首次创建使用
*/
public static function getInstance()
{
$subClass = static::class;
if (!isset(self::$instances[$subClass])) {
self::$instances[$subClass] = new static();
}
return self::$instances[$subClass];
}
/**
* 构造函数需要是 private 访问权限的,避免外部通过 new 创建实例;
* 要想使用实例,必须从 Singleton::getInstance() 获取
*/
private function __construct()
{
}
/**
* 防止克隆函数,这将又会创造一个实例
*/
public function __clone()
{
throw new Exception("Singleton can not clone");
}
/**
* 防止序列化
*/
public function __wakeup()
{
throw new Exception("Singleton can not unserialize");
}
}
2.3 基于单例基类实现日志访问器
- 通常日志管理实例只有一个,防止文件写入冲突,可以利用单例模式实现
namespace App\Creational\Singleton;
/**
* 单例日志记录器: SingletonLog
*/
class SingletonLog extends SingletonBase {
/**
* 文件句柄
*/
private $fileHandle;
/**
* 只会被实例化一次
*/
protected function __construct()
{
$this->fileHandle = fopen('php://stdout', 'w');
}
/**
* 写入数据
*/
public function writeLog(string $message): void
{
fwrite($this->fileHandle, sprintf("[cli-%s][%s] %s:%s message:%s",
getmypid(), date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, $message) . PHP_EOL);
}
/**
* 快捷方式:写入日志
*
* @param string $message
* @return void
*/
public static function log(string $message): void
{
$logger = static::getInstance();
$logger->writeLog($message);
}
}
- 测试一下
use App\Creational\Singleton\Singleton;
use App\Creational\Singleton\SingletonLog;
main();
function main()
{
try {
$instance = Singleton::getInstance();
$instance->handle();
$logIns1 = SingletonLog::getInstance();
$logIns1->writeLog("111");
$logIns2 = SingletonLog::getInstance();
$logIns2->writeLog("222");
if($logIns1 === $logIns2) {
SingletonLog::log("logIns1 === logIns2");
} else {
SingletonLog::log("logIns1 !== logIns2");
}
// 禁止克隆
// $instanceClone = clone $instance;
// 禁止序列化
// $str = serialize($instance);
// $obj = unserialize($str);
} catch (Exception $th) {
echo sprintf("Error message:%s\n", $th->getMessage());
}
}
3、Application
3.1 Laravel 中的单例体现
参考
Laravel 6.0
, Laravel 中单例是通过服务容器实现的,它可以自动管理对象的生命周期。
服务容器是一个用于解决类依赖关系的工具,它可以自动实例化类,并且支持单例模式。
主要流程
使用:在 bootstrap/app.php
引导启动中,应用程序自动注册和绑定服务,如你所见,通过 $app->singleton()
把 Illuminate\Contracts\Http\Kernel
绑定的应用中的 App\Http\Kernel
具体内核类中,从而实现 HTTP 内核的单例模式,然后在 index.php
中就会用到, 通过 $app->make()
, 服务容器会自动解析 Illuminate\Contracts\Http\Kernel
接口,并返回一个已经实例化的 App\Http\Kernel
类的实例对象,这个对象是在服务容器中注册的单例对象。
- boostrap 中进行绑定具体类
$app->singleton()
- 删除之前的实例,将抽象类型通过
bind()
绑定到具体类型上 - 如果之前被
resolved()
解析过- 通过
rebound()
就触发回调重新绑定 - 调用
make()
- 重新进行解析
resolve()
- 根据给定的类型
abstract
,解析成具体的类型concrete
,调用build()
方法 返回单例对象
- 通过
- 删除之前的实例,将抽象类型通过
- index 中进行调用单例对象
$app->make()
resolve()
通过抽象类型解析到具体类型,并且获取单例对象
3.2 Laravel 代码解读
3.2.1 单例绑定,抽象类型绑定到具体类型上
- Laravel 的启动文件,绑定一些重要的接口
$app->singleton()
// bootstrap\app.php
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
3.2.2 获取单例对象
- Laravel 入口文件就是用到了 Kernel 的单例对象
$app->make()
// public\index.php
// 使用服务容器来解析接口,并返回其实现类的实例对象
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// 处理 HTTP 请求
$response = $kernel->handle(
// 创建 HTTP 请求
$request = Illuminate\Http\Request::capture()
);
// 发送 HTTP 响应
$response->send();
// 终止 HTTP 请求
$kernel->terminate($request, $response);
3.2.3 singleton()
将抽象类绑定到具体对象上,并且设置单例
// vendor\laravel\framework\src\Illuminate\Container\Container.php
/**
* 一个具体类型绑定到一个抽象标识符上,并指定该实例在容器中只创建一次
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
- 删除之前的实例,将抽象类型进行
bind
到具体类型上
// vendor\laravel\framework\src\Illuminate\Container\Container.php
/**
* 向容器注册绑定。
*
* @param string $abstract 抽象标识符
* @param \Closure|string|null $concrete 服务的具体实现类或者创建实例的回调函数
* @param bool $shared 该服务是否是共享的单例对象
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
// 先删除之前旧的实例
$this->dropStaleInstances($abstract);
// 如果没有具体的类型,先设置成抽象类型
if (is_null($concrete)) {
$concrete = $abstract;
}
// 如果不是匿名函数 Closure ,那就是一个抽象的类名,那把他封装一层,也成为匿名函数
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
// 绑定保存到服务容器中
// compact 将 $concrete 和 $shared 变量打包成一个关联数组
$this->bindings[$abstract] = compact('concrete', 'shared');
// 如果抽象已经被解析过
if ($this->resolved($abstract)) {
// 重新绑定,抽象类型进行回调
$this->rebound($abstract);
}
}
- 重新进行绑定回调
// vendor\laravel\framework\src\Illuminate\Container\Container.php
/**
* 重新绑定,触发抽象类型的回调
*
* @param string $abstract
* @return void
*/
protected function rebound($abstract)
{
$instance = $this->make($abstract);
foreach ($this->getReboundCallbacks($abstract) as $callback) {
call_user_func($callback, $this, $instance);
}
}
3.2.4 make()
从容器中解析出来给定的类型,并且返回单例对象
- make
// vendor\laravel\framework\src\Illuminate\Container\Container.php
/**
* 从容器中解析出来给定的类型
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
- 根据给定的类型
abstract
,解析成具体的类型concrete
,调用build()
方法 返回单例对象
// vendor\laravel\framework\src\Illuminate\Container\Container.php
/**
* 从容器中给定类型解析成具体的实例,并且 isShared () 方法来判断是否为单例,需要缓存
*
* @param string $abstract 解析的类型的抽象标识符
* @param array $parameters 解析时需要传递的参数
* @param bool $raiseEvents 是否触发解析事件
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
// 转化成对应的别名
$abstract = $this->getAlias($abstract);
// 是否根据上下文构建
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// 如果这个类型是单例,并且不需要根据上下文构建,就直接返回
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
// 需要上下文的参数
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// 确定为绑定注册的具体类型进行实例化
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
// 如果还不是具体类型,那就递归处理
$object = $this->make($concrete);
}
// 是否有扩展器,需要额外处理
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// 如果注册的是单例,那么就需要缓存它
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
// 实例化完成,已经将抽象类型解析为具体的对象
$this->resolved[$abstract] = true;
// 移除对应的参数
array_pop($this->with);
return $object;
}
3.2.5 build()
具体实例化单例对象的地方
- 真正创建单例的地方,利用 具体类型的反射 进行单例的实例化,解决单例类中的依赖问题
/**
* 创建具体类型的实例对象,并自动解决依赖关系。
*
* @param string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// 如果是闭包,就直接执行,返回回调函数的内容
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
// 获取类型的反射 ReflectionClass 对象
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
// 如果类型无法实例化,就退出
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
// 获取构造函数
$constructor = $reflector->getConstructor();
// 如果没有构造函数,没有依赖,可以直接进行实例化
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// 获取依赖的参数
$dependencies = $constructor->getParameters();
// 获取构造函数所依赖的所有参数,然后构建一个新的实例
try {
// 处理相关的依赖
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
// 把依赖的参数的实例,传递给 反射对象,用来实例化 $concrete
return $reflector->newInstanceArgs($instances);
}
4、 Summary
单例模式,保证全局只有一个实例,提供唯一的访问点 ,从而避免多次创建相同的对象产生的开销和浪费。
- 为什么要用单例模式?
- 避免应用中到处都是 new 新的对象,可以用单例模式提供统一的访问点
- 如何使用单例模式?
- 通过私有构造函数,提供公共的静态方法来获取实例,实例保存在私有静态属性中,克隆方法私有,序列化方法私有,保证唯一。
- 是不是每个类都弄个单例模式,岂不是很省资源?
- 通常来说,比如数据库连接、日志写入、配置读取等,这些都可以用到单例模式,因为在这些对象生命周期会延长到整个应用的生命周期。
- 单例模式是自己创建一个对象,这个类责任太重了?
- 可以用工厂模式或者IOC容器方法来统一创建单例对象。