php设计模式之单例模式

在面向对象中,一个对象只负责一个特定的任务通常是一种很好的选择。例如:只想让一个对象去访问数据库。单例模式是一种职责模式,他将创建的对象委托到一个单一的访问点上。在任何时候,应用程序中都只有这个类仅有的一个实例存在。这可以防止我们去打开数据库的多个连接或者不必要使用多余的系统资源。在更加复杂的系统中,使用单例模式在维持应用程序状态同步方面也尤为重要。
同步一般是对于语言多线程方面的应用,PHP 本身不支持多线程。

没有使用单例模式的内存结构图

使用单例模式的内存结构图

对比两张内存图的结构,我们可以了解一下信息:

  1. 没有使用单例模式的构造图很混乱
  2. 没有使用单例模式的数据库连接对象会随着系统的扩展而变多。
  3. 没有使用单例模式的数据库链接因为有多个对象操作不同的 SQL,可能会导致业务逻辑的异常或者是同步的异常。

只有一个类的数据库链接

demo1.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
header('Content-Type:text/html; charset=utf-8');

class DB{
private $db;

public function __construct(){
try{
$this->db = new PDO('mysql:host=localhost;dbname=db', 'root', 'password');
echo '创建了一次数据库连接对象';
}catch(PDOException $e){
exit($e->getMessage());
}
}

public function query($sql){
return $this->db->query($sql);
}
}

$db = new Article();
var_dump($db->query('SELECT * FROM user'));

增加了一些业务类

demo2.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
<?php
header('Content-Type:text/html; charset=utf-8');

class DB{
private $db;

public function __construct(){
try{
$this->db = new PDO('mysql:host=localhost;dbname=db', 'root', 'password');
echo '创建了一次数据库连接对象';
}catch(PDOException $e){
exit($e->getMessage());
}
}

public function query($sql){
return $this->db->query($sql);
}
}

class Article{
private $db;

public function __construct(){
$this->db = new DB();
}

public function query($sql){
return $this->db->query($sql);
}
}

class User{
private $db;

public function __construct(){
$this->db = new DB();
}

public function query($sql){
return $this->db->query($sql);
}
}

$article = new Article();
var_dump($article->query('SELECT * FROM article'));

echo '<br />';

$user = new User();
var_dump($user->query('SELECT * FROM user'));

使用全局变量来保证单例

demo3.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
<?php
header('Content-Type:text/html; charset=utf-8');

class DB{
private $db;

public function __construct(){
try{
$this->db = new PDO('mysql:host=localhost;dbname=db', 'root', 'password');
echo '创建了一个数据库连接对象';
}catch(PDOException $e){
exit($e->getMessage());
}
}

public function query($sql){
return $this->db->query($sql);
}
}

$GLOBALS['db'] = new DB();

class Article{
private $db;

public function __construct(){
$this->db = $GLOBALS['db'];
}

public function query($sql){
return $this->db->query($sql);
}
}

class User{
private $db;

public function __construct(){
$this->db = $GLOBALS['db'];
}

public function query($sql){
return $this->db->query($sql);
}
}

$article = new Article();
var_dump($article->query('SELECT * FROM article'));

echo '<br />';

$user = new User();
var_dump($user->query('SELECT * FROM user'));

虽然全局变量可以实现单例,让整个内存流程清晰,让对象保持一个,但这是有代价的:

  1. 全局变量处在特定的区域环境,破坏了整个类的封装;
  2. 如果新的应用程序无法保证一开始就定了相同的全局变量,那么这个类依赖全局变量,就无法从一个应用中提取出来应用到新的应用程序上。
  3. 全局变量不受保护,也就是说,可以任意存放和修改。一旦依赖全局,随着项目的扩展,冲突在所难免。因为全局不变量不像类中的成员属性,如果冲突会提示警告报错,而全局变量不会。
  4. 全局变量,并没有真正实现单例,如果在程序中不遵循类库提供个规范,而自己创建类,则还是会产生多个对象。

使用单例模式来构建

单例类所需的三个公共元素:

  1. 必须拥有一个构造方法,并且必须被标记为 private;
  2. 拥有一个保存类的实例的静态成员属性(字段);
  3. 拥有一个访问这个实例的公共的静态方法。

demo4.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
<?php
header('Content-Type:text/html; charset=utf-8');

class DB{
private $db;
// 静态是通过类::字段直接访问的,private表示外部不能访问
static private $instance;
// 访问这个实例的公共静态方法
static public function getInstance(){
//如果对象没有创建,就创建它,如果创建了,就直接返回
if(!(self::$instance instanceof self)){
self::$instance = new self();
}
return self::$instance;
}
// 单一职责问题,私有化克隆
private function __clone(){}
// 私有化构造方法
private function __construct(){
try{
$this->db = new PDO('mysql:host=localhost;dbname=db', 'root', 'password');
echo '创建了一个数据库连接对象';
}catch(PDOException $e){
exit($e->getMessage());
}
}

public function query($sql){
return $this->db->query($sql);
}
}

$db = DB::getInstance();
var_dump($db->query('SELECT * FROM article'));

echo '<br />';

$db = new User();
var_dump($db->query('SELECT * FROM user'));

单例模式可以很好的替代全局变量。虽然单例模式也存在和全局变量一样的缺点 ,比如依赖性,因此我们应当小心谨慎的使用单例模式。