设计模式之单例(Singleton)模式
意图
单例(Singleton)模式一种创建型的设计模式,可以确保类只有一个实例,同时为该实例提供全局访问点。
问题描述
单例模式同时解决了两个问题,违反了单一职责原则。
确保一个类只有一个实例。为什么有人要控制一个类由多少各实例呢?最常见的原因是控制对某些共享资源的访问,例如数据库或文件。
它的工作原理如下:想象一下你创建了一个对象,但过了一段时间后决定创建一个新对象,你将获得已创建的对象,而不是接收新对象。
请注意,这种行为是不可能用常规构造函数实现的,因为构造函数调用必须始终按设计返回一个新对象。
请注意,这种行为是不可能用常规构造函数实现的,因为构造函数调用必须始终按设计返回一个新对象。
提供该实例的全局访问点。还记得用来存储一些基本对象的那些全局变量吗?虽然它们非常方便,但也非常不安全,因为任何代码都可能覆盖这些变量的内容并使应用程序崩溃。
就像全局变量一样,单例(Singleton)模式允许从程序中的任何位置访问某些对象。但是,它也可以保护该实例不被其他代码覆盖。
这个问题还有另一面:你不希望解决问题 #1 的代码分散在你的程序中的各个地方。将它放在一个类中要好得多,尤其是如果代码的其余部分已经依赖于它的话。
如今,单例(Singleton)模式已经变得如此流行,以至于人们可能会将一些即使只解决列出的问题之一也称之9为单例(Singleton)。
方案
单例(Singleton)的所有实现都有以下两个共同步骤:
- 将默认构造函数设为私有构造函数,以防止其他对象将
new
运算符调用单例类。 - 创建一个充当构造函数的静态创建方法。在后台,此方法调用私有构造函数来创建对象并将其保存在静态字段中。接下来对该方法的所有调用都返回缓存的对象。
如果您的代码可以访问单例(Singleton)类,那么它就可以调用单例(Singleton)的静态方法。因此,每当调用该方法时,总是返回相同的对象。
真实世界类比
政府是单例模式的一个很好的例子。一个国家只能有一个官方政府。无论组建政府的个人的个人身份如何,“X 政府”这一名称都是一个全球接入点,用于识别负责人群体。
结构
单例(Singleton)类声明了静态方法 getInstance
,该方法返回自身类的同一个实例。
单例(Singleton)的构造函数应该对客户端代码隐藏。调用 getInstance
方法应该是获取单例(Singleton)对象的唯一方法。
伪代码
本例中,数据库链接扮演者一个单例(Singleton)。该类没有公开的构造函数,因此获取其对象的唯一方式是调用 getInstance
方法。该方法缓存了第一次创建的对象并将其返回给后续的调用。
// The Database class defines the `getInstance` method that lets
// clients access the same instance of a database connection
// throughout the program.
class Database is
// The field for storing the singleton instance should be
// declared static.
private static field instance: Database
// The singleton's constructor should always be private to
// prevent direct construction calls with the `new`
// operator.
private constructor Database() is
// Some initialization code, such as the actual
// connection to a database server.
// ...
// The static method that controls access to the singleton
// instance.
public static method getInstance() is
if (Database.instance == null) then
acquireThreadLock() and then
// Ensure that the instance hasn't yet been
// initialized by another thread while this one
// has been waiting for the lock's release.
if (Database.instance == null) then
Database.instance = new Database()
return Database.instance
// Finally, any singleton should define some business logic
// which can be executed on its instance.
public method query(sql) is
// For instance, all database queries of an app go
// through this method. Therefore, you can place
// throttling or caching logic here.
// ...
class Application is
method main() is
Database foo = Database.getInstance()
foo.query("SELECT ...")
// ...
Database bar = Database.getInstance()
bar.query("SELECT ...")
// The variable `bar` will contain the same object as
// the variable `foo`.
适用场景
当程序中的某个类只应该一个实例对所有客户端可用时,比如,程序多个地方分享的一个数据库对象,可以使用单例模式。
单例(Singleton)模式禁用了除特殊创建方法之外的所有其他创建类对象的方法。该方法要么创建一个新对象,要么返回一个已经创建的现有对象。
当需要对全局变量进行更严格的控制时,请使用单例模式。
与全局变量不同,单例(Singleton)模式保证一个类只有一个实例。除了单例(Singleton)类本身之外,没有任何东西可以替换缓存的实例。
请注意,你可以随时调整此限制,并允许创建任意数量的单例(Singleton)实例。唯一需要更改的代码是 getInstance
方法的主体。
如何实现
在类中添加一个私有的静态字段,用以存储单例实例。
声明一个共有的静态创建方法,以获取单例实例。
在静态方法内实现延迟初始化。第一次调用时创建一个新对象并将其放到静态字段中。后续的调用中该方法应该总是返回那个实例。
使构造函数成为私有。类的静态函数仍然可以调用构造函数,但其他对象不行。
仔细检查客户端代码,将对单例构造函数的所有直接调用替换为对其静态创建方法的调用。
优缺点
- ✔️可以确保该类只有一个实例。
- ✔️你得到了该实例的全局访问点。
- ✔️单例对象只会在第一次请求的时候初始化。
- ❌违反了单一职责原则。该模式同时解决了两个问题。
❌单例模式可能掩盖了糟糕的设计,例如,当程序的组件之间彼此了解太多等。
❌该模式在多线程环境中需要进行特殊处理,这样多个线程才不会多次创建单例对象。
❌对但单例的客户端代码进行单元测试可能很难,因为许多测试框架在生成模拟对象时依赖于继承。由于单例类的构造函数是私有的,并且在大多数语言中不可能覆盖静态方法,因此需要想出一种创造性的方法来模拟单例。或者就是不写测试。或者不要使用单例模式。
与其他模式的关系
门面(Facade)类可以转换成单例(Singleton), 因为在大多数情况下,一个 Facade 对象就足够了。
如果设法将私有共享状态的对象减少为一个享元(Flyweight)对象,那么享元(Flyweight)模式将与单例(Singleton)相似。不过这两个模式由两个根本区别:
- 单例只能由一个实例,而 一个 Flyweight 类可以有多个具有不同内部状态的实例。
- 单例(Singleton)对象可以是可变的。Flyweight 对象是不可变的。
抽象工厂(Abstract Factory)、建造者(Builder)和原型(Prototype)可以使用单例(Singleton)实现。
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\Singleton\Conceptual;
/**
* The Singleton class defines the `GetInstance` method that serves as an
* alternative to constructor and lets clients access the same instance of this
* class over and over.
*/
class Singleton
{
/**
* The Singleton's instance is stored in a static field. This field is an
* array, because we'll allow our Singleton to have subclasses. Each item in
* this array will be an instance of a specific Singleton's subclass. You'll
* see how this works in a moment.
*/
private static $instances = [];
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
protected function __construct() { }
/**
* Singletons should not be cloneable.
*/
protected function __clone() { }
/**
* Singletons should not be restorable from strings.
*/
public function __wakeup()
{
throw new \Exception("Cannot unserialize a singleton.");
}
/**
* This is the static method that controls the access to the singleton
* instance. On the first run, it creates a singleton object and places it
* into the static field. On subsequent runs, it returns the client existing
* object stored in the static field.
*
* This implementation lets you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static function getInstance(): Singleton
{
$cls = static::class;
if (!isset(self::$instances[$cls])) {
self::$instances[$cls] = new static();
}
return self::$instances[$cls];
}
/**
* Finally, any singleton should define some business logic, which can be
* executed on its instance.
*/
public function someBusinessLogic()
{
// ...
}
}
/**
* The client code.
*/
function clientCode()
{
$s1 = Singleton::getInstance();
$s2 = Singleton::getInstance();
if ($s1 === $s2) {
echo "Singleton works, both variables contain the same instance.";
} else {
echo "Singleton failed, variables contain different instances.";
}
}
clientCode();
Output.txt: 执行结果
Singleton works, both variables contain the same instance.