单例模式 (singleton)

未匹配的标注

手握设计模式宝典 - 单例模式 singleton

Design-Pattern - Creational - Singleton

  • Title: 《手握设计模式宝典》 之 单例模式 singleton
  • Tag: Design-PatternCreationalSingleton单例模式设计模式
  • Author: Tacks
  • Create-Date: 2023-08-03
  • Update-Date: 2023-08-10

0、REF

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容器方法来统一创建单例对象。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~