0%

PHP Patterns: Singleton

Talk is cheap, show me the code.

单例在 PHP 中的实现

Original
单例是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

单例拥有与全局变量相同的优缺点。尽管它们非常有用,但却会破坏代码的模块化特性。

你可能会在一些其他情况下使用依赖于单例的类。你也将必须使用单例类。绝大多数情况下,该限制会在创建单元测试时出现。

在 PHP 中使用模式

复杂度:
流行度:

使用示例:
许多开发者将单例模式视为一种反模式。因此它在 PHP 代码中的使用频率正在逐步减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?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:

1
Singleton works, both variables contain the same instance.

真实世界示例

单例模式由于限制了代码复用,且让单元测试复杂化而名声不佳。但它在有些情况下仍然非常实用,特别是在需要控制一些共享资源时十分方便。例如,全局日志对象必须对日志文件的访问权限进行控制。另一个例子:共享的运行时配置存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<?php

namespace RefactoringGuru\Singleton\RealWorld;

/**
* If you need to support several types of Singletons in your app, you can
* define the basic features of the Singleton in a base class, while moving the
* actual business logic (like logging) to subclasses.
*/
class Singleton
{
/**
* The actual singleton's instance almost always resides inside a static
* field. In this case, the static field is an array, where each subclass of
* the Singleton stores its own instance.
*/
private static $instances = [];

/**
* Singleton's constructor should not be public. However, it can't be
* private either if we want to allow subclassing.
*/
protected function __construct() { }

/**
* Cloning and unserialization are not permitted for singletons.
*/
protected function __clone() { }

public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}

/**
* The method you use to get the Singleton's instance.
*/
public static function getInstance()
{
$subclass = static::class;
if (!isset(self::$instances[$subclass])) {
// Note that here we use the "static" keyword instead of the actual
// class name. In this context, the "static" keyword means "the name
// of the current class". That detail is important because when the
// method is called on the subclass, we want an instance of that
// subclass to be created here.

self::$instances[$subclass] = new static;
}
return self::$instances[$subclass];
}
}

/**
* The logging class is the most known and praised use of the Singleton pattern.
* In most cases, you need a single logging object that writes to a single log
* file (control over shared resource). You also need a convenient way to access
* that instance from any context of your app (global access point).
*/
class Logger extends Singleton
{
/**
* A file pointer resource of the log file.
*/
private $fileHandle;

/**
* Since the Singleton's constructor is called only once, just a single file
* resource is opened at all times.
*
* Note, for the sake of simplicity, we open the console stream instead of
* the actual file here.
*/
protected function __construct()
{
$this->fileHandle = fopen('php://stdout', 'w');
}

/**
* Write a log entry to the opened file resource.
*/
public function writeLog(string $message): void
{
$date = date('Y-m-d');
fwrite($this->fileHandle, "$date: $message\n");
}

/**
* Just a handy shortcut to reduce the amount of code needed to log messages
* from the client code.
*/
public static function log(string $message): void
{
$logger = static::getInstance();
$logger->writeLog($message);
}
}

/**
* Applying the Singleton pattern to the configuration storage is also a common
* practice. Often you need to access application configurations from a lot of
* different places of the program. Singleton gives you that comfort.
*/
class Config extends Singleton
{
private $hashmap = [];

public function getValue(string $key): string
{
return $this->hashmap[$key];
}

public function setValue(string $key, string $value): void
{
$this->hashmap[$key] = $value;
}
}

/**
* The client code.
*/
Logger::log("Started!");

// Compare values of Logger singleton.
$l1 = Logger::getInstance();
$l2 = Logger::getInstance();
if ($l1 === $l2) {
Logger::log("Logger has a single instance.");
} else {
Logger::log("Loggers are different.");
}

// Check how Config singleton saves data...
$config1 = Config::getInstance();
$login = "test_login";
$password = "test_password";
$config1->setValue("login", $login);
$config1->setValue("password", $password);
// ...and restores it.
$config2 = Config::getInstance();
if ($login == $config2->getValue("login") &&
$password == $config2->getValue("password")
) {
Logger::log("Config singleton also works fine.");
}

Logger::log("Finished!");

Output:

1
2
3
4
2018-06-04: Started!
2018-06-04: Logger has a single instance.
2018-06-04: Config singleton also works fine.
2018-06-04: Finished!

More Examples


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php
// 单列模式

//1.普通类
class singleton{

}

$s1 = new singleton();
$s2 = new singleton();
//注意,2个变量是同1个对象的时候才全等
if ($s1 === $s2) {
echo '是一个对象';
}else{
echo '不是一个对象';
}

//2.封锁new操作
class singleton{
protected function __construct(){}
}
$s1 = new singleton(); //PHP Fatal error: Call to protected singleton::__construct()

//3.留个接口来new对象
class singleton{
protected function __construct(){}

public static function getIns(){
return new self();
}
}

$s1 = singleton::getIns();
$s2 = singleton::getIns();
if ($s1 === $s2) {
echo '是一个对象';
}else{
echo '不是一个对象';
}

//4.getIns先判断实例
class singleton{

protected static $ins = null;

private function __construct(){}

public static function getIns(){
if (self::$ins === null) {
self::$ins = new self();
}
return self::$ins;
}
}

$s1 = singleton::getIns();
$s2 = singleton::getIns();
if ($s1 === $s2) {
echo '是一个对象';
}else{
echo '不是一个对象';
}

//继承
class A extends singleton{
public function __construct(){}
}
echo '<br>';
$s1 = new A();
$s2 = new A();
if ($s1 === $s2) {
echo '是同一个对象';
}else{
echo '不是同一个对象';
}


//5.防止继承时被修改了权限
class singleton{

protected static $ins = null;

//方法加final则方法不能被覆盖,类加final则类不能被继承
final private function __construct(){}

public static function getIns(){
if (self::$ins === null) {
self::$ins = new self();
}
return self::$ins;
}
}

$s1 = singleton::getIns();
$s2 = singleton::getIns();
if ($s1 === $s2) {
echo '是同一个对象';
}else{
echo '不是同一个对象';
}

// 继承
class A extends singleton{
public function __construct(){}
}
//Cannot override final method singleton::__construct()

echo '<hr>';
$s1 = singleton::getIns();
$s2 = clone $s1;
if ($s1 === $s2) {
echo '是同一个对象';
}else{
echo '不是同一个对象';
}


//6.防止被clone
class singleton{

protected static $ins = null;

//方法加final则方法不能被覆盖,类加final则类不能被继承
final private function __construct(){}

public static function getIns(){
if (self::$ins === null) {
self::$ins = new self();
}
return self::$ins;
}

// 封锁clone
final private function __clone(){}
}

$s1 = singleton::getIns();
$s2 = clone $s1; //Call to private singleton::__clone() from context
if ($s1 === $s2) {
echo '是同一个对象';
}else{
echo '不是同一个对象';
}

More quotations:

stackoverflow

Singletons have very little - if not to say no - use in PHP.

In languages where objects live in shared memory, Singletons can be used to keep memory usage low. Instead of creating two objects, you reference an existing instance from the globally shared application memory. In PHP there is no such application memory. A Singleton created in one Request lives for exactly that request. A Singleton created in another Request done at the same time is still a completely different instance. Thus, one of the two main purposes of a Singleton is not applicable here.

In addition, many of the objects that can conceptually exist only once in your application do not necessarily require a language mechanism to enforce this. If you need only one instance, then don't instantiate another. It's only when you may have no other instance, e.g. when kittens die when you create a second instance, that you might have a valid Use Case for a Singleton.

The other purpose would be to have a global access point to an instance within the same Request. While this might sound desirable, it really isnt, because it creates coupling to the global scope (like any globals and statics). This makes Unit-Testing harder and your application in general less maintainable. There is ways to mitigate this, but in general, if you need to have the same instance in many classes, use Dependency Injection.

See my slides for Singletons in PHP - Why they are bad and how you can eliminate them from your applications for additional information.

Even Erich Gamma, one of the Singleton pattern's inventors, doubts this pattern nowadays:

I'm in favor of dropping Singleton. Its use is almost always a design smell

Further reading

How is testing the registry pattern or singleton hard in PHP?
What are the disadvantages of using a PHP database class as a singleton?
Database abstraction class design using PHP PDO
Would singleton be a good design pattern for a microblogging site?
Modifying a class to encapsulate instead of inherit
How to access an object from another class?
Why Singletons have no use in PHP
The Clean Code Talks - Singletons and Global State
If, after the above, you still need help deciding: Singleton Decision Diagram