Debug Hacks

谦虚其心,宏大其量.

Php中的单例模式面面观

最近在面试时发现好多人都喜欢在简历中添加“熟悉各种设计模式”那一项。但也不乏对于最为简单的单例,也仅限于书本上的一点认知,所以扯一扯php中单例模式的几种使用场景。

何为单例模式

单例模式属于创建型的应用模式,可以确保某一个类只有一个实例。这在获取数据库,缓存服务器的连接句柄,以及获取配置信息方面非常方便。

使用单例模式,可以减少在一次web请求中多次连接数据库或者解析配置文件等操作对系统资源的消耗,也可以避免大量的new操作。

其UML结构图如下:

在php中实现单例模式需要注意:

  • 需要一个静态变量保存实例
  • 需要提供一个公共的静态方法,用于返回需要获取的实例
  • 需要防止外部程序通过new和clone等操作产生相关实例,从而失去单例模式的意义

代码实现解析

一个单例模式的php实现大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton
{
    private static $_instance = null;

    private function __construct()
    {
    }

    public function __clone()
    {
        trigger_error("clone method is not allowed.", E_USER_ERROR);
    }

    public static function getInstance()
    {
        if (self::$_instance === null) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }
}

上述代码通过静态成员变量保存创建的实例,通过将构造函数设置为私有的,并使用魔术方法__clone“重载”clone方法以避免通过外部程序获取相关实例。

由于php中的静态变量包括:静态全局变量静态局部变量静态成员变量,所以也可以通过静态局部变量来保存创建的实例。但是在子类中获取创建的实例时,会遇到一些问题。示例如下:

使用静态局部变量

singleton-static.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
class Singleton
{
    private $_handle = null;

    private function __construct()
    {
        $this->_handle = new StdClass();
    }

    public function __clone()
    {
        trigger_error("clone method is not allowed.", E_USER_ERROR);
    }

    public static function getInstance()
    {
        static $_instance = null;

        if ($_instance === null) {
            $_instance = new self();
        }

        return $_instance;
    }

    public function getObj()
    {
        return $this->_handle;
    }
}

class UserModel extends Singleton
{
    public static function getUser()
    {
        var_dump(self::getInstance()->getObj());
        //logic no care
    }
}

class GroupModel extends Singleton
{
    public static function getGroup()
    {
        var_dump(self::getInstance()->getObj());
        //logic no care
    }
}

UserModel::getUser();
GroupModel::getGroup();
$ php /.singleton-static.php
object(Singleton)#2 (0) {
}
object(Singleton)#4 (0) {
}

此例没有获取到唯一实例,因为局部静态变量,也就是在函数中定义的静态变量。其信息是存储在zend vm为每个函数分配的一个私有符号表中。

php中当继承发生时,会进行函数的合并。UserModel中调用的,实际上是合并在UserModel中的getInstance,GroupModel中调用的,也是合并在GroupModel中的getInstance方法。

因为是在两个不同方法的私有符号表中获取数据,所以取到的不是同一个实例。

可以将self换成parent试试

使用静态成员变量

singleton-static-member.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
class Singleton
{
    private $_handle = null;

    private static $_instance = null;

    private function __construct()
    {
        $this->_handle = new StdClass();
    }

    public function __clone()
    {
        trigger_error("clone method is not allowed.", E_USER_ERROR);
    }

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

    public function getObj()
    {
        return $this->_handle;
    }
}

class UserModel extends Singleton
{
    public static function getUser()
    {
        var_dump(self::getInstance()->getObj());
        //logic no care
    }
}

class GroupModel extends Singleton
{
    public static function getGroup()
    {
        var_dump(self::getInstance()->getObj());
        //logic no care
    }
}
UserModel::getUser();
GroupModel::getGroup();
$ php /.singleton-static-member.php
object(Singleton)#2 (0) {
}
object(Singleton)#2 (0) {
}

此时获取到了唯一实例,因为静态成员变量的信息是存储在类结构的 default_static_members 字段,为所有实例所共用。

编写一个单例的基类

有时我们的项目中有个缓存类,还有数据库类,还有个文件操作类。想让这三个类都是单例的话,我们需要实现三个单例,这有点繁琐。于是我们可以创建一个单例的基类,要实现单例的类继承这个基类即可。直接上代码:

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
<?php
class Singleton
{
    private static $_instances = array();

    protected function __construct()
    {
    }

    final public function __clone()
    {
        trigger_error("clone method is not allowed.", E_USER_ERROR);
    }

    final public static function getInstance()
    {
        $c = get_called_class();

        if(!isset(self::$_instances[$c])) {
            self::$_instances[$c] = new $c;
        }

        return self::$_instances[$c];
    }
}

class Cache Extends Singleton
{
    private $_handle = null;

    protected function __construct()
    {
        $this->_connect();
    }

    private function _connect()
    {
        $this->_handle = new StdClass();
    }

    public function getHandle()
    {
        return $this->_handle;
    }
}

class Db Extends Singleton
{
    private $_handle = null;

    protected function __construct()
    {
        $this->_connect();
    }

    private function _connect()
    {
        $this->_handle = new StdClass();
    }

    public function getHandle()
    {
        return $this->_handle;
    }
}

reference:

[^1] http://www.php-internals.com/book/?p=chapt05/05-04-class-inherit-abstract

[^2] http://zh.wikipedia.org/zh/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F

[^3] http://www.phptherightway.com/pages/Design-Patterns.html

[^4] http://rancoud.com/read-phps-opcode/