实现一个 PHP 单例:使用静态类属性还是静态方法变量?

13

所以,我一直像这样实现单例:

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

然而,我最近想到我也可以用成员级别的静态变量来实现它:

class Singleton {
    public static function getInstance() {
        //oops - can't assign expression here!
        static $instance = null; // = new Singleton();
        if ($instance === null) $instance = new Singleton();
        return $instance;
    }
    private function __construct() { }
}

对我来说,这个实现更清晰,因为它不会弄乱类,而且我不需要进行任何显式的存在检查,但是因为我从未在其他地方见过这个实现方式,所以我想知道:使用第二种实现方法是否有什么问题?

第二种方法也可以作为普通函数实现,我有时会这样使用,例如 i()->DB() - Alix Axel
@Alix:我不明白。如果getInstance函数不是静态的,它怎么能与单例相关呢? - Austin Hyde
1
@Austin Hyde:他指的是一个普通函数,而不是一个方法。然而,这将违反单例模式的规则,因为它需要一个公共构造函数来实现。它只是一个方便的函数,可以避免每次都要实例化对象。 - ircmaxell
1
@ircmaxell:啊,这样就更有意义了。感谢您的澄清。 - Austin Hyde
@ircmaxell:是的,但是使用“普通”的单例模式也可以违反同样的规则,不是吗? - Alix Axel
显示剩余2条评论
4个回答

12

使用 class 属性。它有一些优点...

class Foo {
    protected static $instance = null;

    public static function instance() {
        if (is_null(self::$instance)) {
            self::$instance = new Foo();
        }
        return self::$instance;
    }
}

首先,使用自动化测试更容易。您可以创建一个mock foo类来“替换”实例,这样依赖于foo的其他类将获得模拟的副本而不是原始类:

class MockFoo extends Foo {
    public static function initialize() {
        self::$instance = new MockFoo();
    }
    public static function deinitialize() {
        self::$instance = null;
    }
}

那么,在你的测试用例中(假设使用phpunit):

protected function setUp() {
    MockFoo::initialize();
}

protected function tearDown() {
    MockFoo::deinitialize();
}

这种方法解决了单例模式常见的测试难题。其次,它让你的代码更加灵活。如果你想在运行时“替换”类中的功能,只需将其子类化并替换 self::$instance即可。

第三,它允许你在其他静态函数中操作实例。对于单个实例类(真正的单例),这并不是什么大问题,因为你可以直接调用 self::instance()。但如果你有多个“命名”拷贝(比如用于数据库连接或需要多个资源的情况,但如果它们已经存在,就不想创建新的),那就会变得复杂,因为你需要跟踪这些名称:

protected static $instances = array();

public static function instance($name) {
    if (!isset(self::$instances[$name])) {
        self::$instances[$name] = new Foo($name);
    }
    return self::$instances[$name];
}

public static function operateOnInstances() {
    foreach (self::$instances as $name => $instance) {
        //Do Something Here
    }
}

还有一点需要注意的是,我不会将构造函数设为私有。这样会导致无法有效地进行扩展或测试。相反,将其设置为受保护的,以便在需要时可以进行子类化并仍然操作父类...


为什么你要让 instance 通过引用返回呢?这样可以在外部替换单例实例,而这是不应该被允许的。否则我会给你点赞的。 - Artefacto

7
您可能是指稍作修改(否则会出现语法错误):
<?php
class Singleton {
    public static function getInstance() {
        static $instance;
        if ($instance === null)
            $instance = new Singleton();
        xdebug_debug_zval('instance');
        return $instance;
    }
    private function __construct() { }
}
$a = Singleton::getInstance();
xdebug_debug_zval('a');
$b = Singleton::getInstance();
xdebug_debug_zval('b');

这将产生以下结果:

实例: (引用计数=2, 是引用=1), 对象(单例模式)[1]

a: (引用计数=1, 是引用=0), 对象(单例模式)[1]

实例: (引用计数=2, 是引用=1), 对象(单例模式)[1]

b: (引用计数=1, 是引用=0), 对象(单例模式)[1]

因此,它的缺点是每次调用都会创建一个新的 zval。这并不是特别严重的问题,所以如果你喜欢的话,可以继续使用。

强制进行 zval 分离的原因是,在 getInstance 内部,$instance 是一个引用(在 =& 的意义上),它的引用计数为 2(一个为方法内部的符号,另一个为静态存储)。由于 getInstance 不返回引用,因此必须分离 zval -- 对于返回值,将创建一个具有引用计数 1 和引用标志清除的新值。


是的,我忘了你不能进行复杂的静态赋值...但我不明白:什么是zval? - Austin Hyde
好的,这样更有意义。但是在PHP 5中,默认情况下对象会被引用传递吗? - Austin Hyde
@Austin 只有引用被分开了。例子中的所有三个引用都指向同一个对象(对象 #1)。 - Artefacto
1
@nikic:谢谢,那个链接帮助我理解了很多。@Artefacto:总的来说,除了一些额外的幕后负担之外,使用方法变量而不是类属性没有任何问题吗? - Austin Hyde
1
@Austin 是的,有一个额外的数据结构被复制了,但性能损失应该非常小。 - Artefacto
显示剩余2条评论

0

经过一些尝试,我能想到的最好方法是这样的:

创建一个名为SingletonBase.php的文件,并将其包含在脚本的根目录中!

代码如下:

abstract class SingletonBase
{
    private static $storage = array();

    public static function Singleton($class)
    {
        if(in_array($class,self::$storage))
        {
            return self::$storage[$class];
        }
        return self::$storage[$class] = new $class();
    }
    public static function storage()
    {
       return self::$storage;
    }
}

然后,对于您想要创建单例的任何类,只需添加此小单例方法即可。

public static function Singleton()
{
    return SingletonBase::Singleton(get_class());
}

这里是一个小例子:

include 'libraries/SingletonBase.resource.php';

class Database
{
    //Add that singleton function.
    public static function Singleton()
    {
        return SingletonBase::Singleton(get_class());
    }

    public function run()
    {
        echo 'running...';
    }
}

$Database = Database::Singleton();

$Database->run();

你可以在任何类中添加这个单例函数,它将每个类仅创建一个实例。

还有另一个想法,你也可以这样做。

if(class_exists('Database'))
{
   $Database = SingletonBase::Singlton('Database');
}

在你的脚本结束时,如果需要,可以进行一些调试。

在你的脚本结束时,只需执行以下操作:

foreach(SingletonBase::storage () as $name => $object)
{
     if(method_exists("debugInfo",$object))
     {
         debug_object($name,$object,$object->debugInfo());
     }
}

因此,这种方法对于调试器获取已初始化的所有类和对象状态的访问权限非常有用。


好主意,这是我没有考虑过的。然而,这并不能真正回答我的问题。 - Austin Hyde
许多其他人已经回答了关于您不能称其为单例模式的问题,因为每次都会创建一个新实例。我的代码是为了向您展示如何编写简洁、少量代码并且作为真正的单例模式,实际上它确实回答了您问题的一部分 :) - RobertPitt
1
这并不会创建一个单例,因为你仍然可以调用 'new Database();'。 - Scott Saunders
你确定这样做可行吗?in_array这个函数好像有问题。in_array是用来检查值是否存在,而不是键。所以,也许你想写的是isset(self::$storage[$class]) - NikiC
也许更好地创建一个“抽象类Singleton”,其中包含“私有函数__clone”和“受保护的__construct”,以及一个方法“public static getInstance”,该方法具有与您的“Singleton”方法相同的功能,但是基于“get_called_class”返回实例? - NikiC
显示剩余5条评论

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接