单例模式

未匹配的标注

单例模式

意图

《设计模式》对单例模式意图的描述如下

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

从意图来看,单例模式的两个核心为「保证一个类仅有一个实例」以及 「提供一个访问它的全局访问点」。

为什么要保证一个类仅有一个实例?有时候是为了控制某些共享资源,例如一个系统仅有一个打印假脱机、文件系统和窗口管理器。

如何保证一个类仅有一个实例?首先,通过构造函数来获取实例肯定是不行的,因为构造函数每次都会生成新的实例对象。其次,单单提供一个全局变量来访问实例也不够,因为不能保证获取的是唯一实例。因此,更好的解决方式就是让类自身来负责保存它的唯一实例,并提供一个访问该实例的方法。这样的话,对于客户端而言,每次获取的都是同一个实例。不过,类既负责了实例的保存,也负责了实例的访问,违反了 单一职责 原则。

实现

单例模式的实现可以分为两种,在类中直接实现单例模式,或者通过继承的方式来实现单例模式。

在类中实现单例模式

image.png

第一种方法就是我们在类中直接实现单例模式,这也是最常用的方法

<?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(){}
}

通过继承实现单例模式

image.png

另外一种方式就是定义一个全局的单例类,子类只需要继承该类就可以实现单例模式。

<?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

应用

image.png

为了更加方便的管理全局变量,可使用单例模式来管理应用配置。但是当我们使用单例模式控制全局变量时,也导致全局变量存在被覆盖的风险。

<?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");

总结

当你要控制某些共享资源时,可使用单例模式。但是单例模式也存在几点不足

  • 单例模式同时负责类的保存与访问,违反了「单一职责」原则,因此,单例模式被认为是反模式。
  • 在多线程环境中,需要对单例模式进行特殊处理,防止多个线程同时创建单例对象。
  • 由于单例模式不能被实例化,将导致单元测试难以进行。

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

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


暂无话题~