【问题标题】:Implement a PHP singleton: static class properties or static method variables?实现 PHP 单例:静态类属性还是静态方法变量?
【发布时间】:2010-07-13 13:24:00
【问题描述】:

所以,我总是像这样实现一个单例:

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()。
  • +1 好问题和有趣的答案
  • @Alix:我不关注。 getInstance 函数如何非静态实现并且仍然适用于单例?
  • @Austin Hyde:他指的是常规函数,而不是方法。但是,这将违反单例规则,即应该只存在一个实例,因为它需要一个公共构造函数。它只是一个方便的函数,不必每次都实例化对象。
  • @ircmaxell:啊。这更有意义。感谢您的澄清。

标签: php design-patterns singleton


【解决方案1】:

您的意思可能是稍作修改(否则我遇到语法错误):

<?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');

这给出了:

实例:(refcount=2, is_ref=1), 对象(单例)[1]

a: (refcount=1, is_ref=0), 对象(单例)[1]

实例:(refcount=2, is_ref=1), 对象(单例)[1]

b: (refcount=1, is_ref=0), 对象(单例)[1]

因此它的缺点是每次调用都会创建一个新的 zval。这不是特别严重,所以如果您愿意,请继续。

强制进行 zval 分离的原因是 getInstance 内部,$instance 是一个引用(在 =&amp; 的意义上,它的引用计数为 2(一个用于方法内部的符号,另一个用于静态存储)。由于getInstance 不通过引用返回,zval 必须分开——为了返回,创建一个新的引用计数为 1 并清除引用标志。

【讨论】:

  • 是的,忘了你不能做不能复杂的静态分配......但我不明白:什么是 zval?
  • 好吧,这更有意义。但是在 PHP 5 中,对象不是默认通过引用传递的吗?
  • @Austin 只是分开的参考。示例中的所有引用都指向同一个对象(对象#1)。
  • @nikic:谢谢,该链接帮助解释了很多。 @Artefacto:总而言之,除了一些额外的幕后包袱之外,在类属性上使用方法变量没有任何问题?
  • @Austin 是的,复制了一个额外的数据结构,但性能损失应该很小。
【解决方案2】:

使用类属性。有几个优点...

class Foo {
    protected static $instance = null;

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

首先,执行自动化测试更容易。您可以创建一个模拟 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通过引用返回。这使您可以在外部替换单例实例,而这是您无法做到的。否则我会支持你。
【解决方案3】:

最干净的解决方案是从类本身中删除单例逻辑(因为它与类本身的工作无关)。

有关有趣的实现,请参见: http://phpgoodness.wordpress.com/2010/07/21/singleton-and-multiton-with-a-different-approach/

【讨论】:

    【解决方案4】:

    在玩了一些我能想到的最好的方法之后是这样的:

    创建一个名为 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();
    

    你可以在你拥有的任何类中添加这个单例函数,它只会为每个类创建 1 个实例。

    只是另一个你也可以做的想法

    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());
         }
    }
    

    所以这个方法非常适合调试器访问所有已初始化的类和对象状态

    【讨论】:

    • 好主意,我没想到。但是,这确实不能回答我的问题。
    • 许多其他人回答了您的问题,因为您每次都创建一个新实例,因此您不能将其称为单例,我的代码是向您展示如何保持清洁,少量编码,并充当真正的单身人士,实际上它确实回答了您的部分问题:)
    • 这不会创建单例,因为您仍然可以调用 'new Database();'
    • 你确定这会起作用吗? in_array 似乎是错误的。 in_array 检查值,而不是键。所以,也许你打算写isset(self::$storage[$class])。
    • 也许更好地创建一个abstract class Singleton,它有private function __clone 和protected __construct 和一个方法public static getInstance,它与你的Singleton 方法具有相同的功能,但是返回实例,基于get_called_class?
    猜你喜欢
    • 2011-06-11
    • 1970-01-01
    • 2018-08-12
    • 1970-01-01
    • 2011-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多