PHP单例模式
设计模式是对软件开发中常见问题的经过良好测试的解决方案。
让我们使用逐步解决问题的方法来实现PHP中最常见的单例模式。
需求场景
在PHP开发(尤其是使用MVC框架)中,我们经常需要include/require一个DB类、一个Upload类或一个Cookie类,如下所示:
<?php require 'DB.class.php'; require 'Upload.class.php'; require 'Cookie.class.php';
但是我们需要确保只有一个DB类或者Cookie类的实例,因为一个实例就足够了,而更多的实例就有问题了。这时单例模式就出现了。
简单地说,单例模式意思是如果一个程序员已经实例化了某个类的一个对象,那么另一个程序员就不能实例化第二个对象。
实现
创建一个通用类
创建了一个名为Singleton的类,就可以实例化任意数量的对象。这里我们创建两个Singleton类对象:
class Singleton {
   
}
$s1 = new Singleton();
$s2 = new Singleton();
$s1和$s2 是两个不同的对象。
我们可以通过使用一个简单的if()语句来判断:
class Singleton {
   
}
$s1 = new Singleton();
$s2 = new Singleton();
if ($s1 === $s2) {
    echo 's1和s2是同一个对象';
} else {
    echo 's1和s2不是同一个对象';
}
禁止new对象操作
对象是由类的构造函数创建的。假如我们把构造函数设置为protected,隐藏在类本身中,这样外部就不能使用它了,但是出现新问题,第一个对象也不能创建了:
class Singleton2 {
    protected function __construct() {
    }
}
$s3 = new Singleton2(); 
//Fatal error: Call to protected Singleton2::__construct() from invalid context
创建一个方法用于在内部创建对象
为了让类用户创建一个对象,我们需要留下一个对象创建“接口”,所以我们添加了一个名为getIns()的新方法,它是公共的(对外开放)和静态的(所以可以使用类名来调用它):
class Singleton3 {
    public static function getIns() {
        return new self();
    }
    protected function __construct() {
    }
}
我们在类中创建一个方法。在getIns()中,我们创建类本身的一个对象,然后将它返回。现在我们再次测试:
class Singleton3 {
    public static function getIns() {
        return new self();
    }
    protected function __construct() {
    }
}
$s4 = Singleton3::getIns();
$s5 = Singleton3::getIns();
if ($s4 === $s5) {
    echo 's4 和 s5是同一个对象';
} else {
    echo 's4 和 s5不是同一个对象';
}
结果还是s4 和 s5不是同一个对象
为什么?因为Singleton3::getIns()被调用两次,因此创建了两个对象。
修改getIns()方法
class Singleton4 {
    protected static $ins = NULL;
    public static function getIns() {
        if (self::$ins === null) {
            self::$ins = new self();
        }
        return self::$ins;
    }
    protected function __construct() {
    }
}
$s6 = Singleton4::getIns();
$s7 = Singleton4::getIns();
if ($s6 === $s7) {
    echo 's6 and s7 are the same object';
} else {
    echo 's6 and s7 are NOT the same object';
}
结果 s6 and s7 are the same object
可以看到,现在我们将类的实例存储在名为$ins的受保护的属性中。然后,当调用getIns()时,我们进行检查:如果$ins为空,那么我们实例化一个类对象。最后,返回self::$ins。
现在这个类现在只有一个实例。
新问题,如果有另一个类叫Multi,它继承了我们原来的类:
class Singleton4 {
    protected static $ins = NULL;
    public static function getIns() {
        if (self::$ins === null) {
            self::$ins = new self();
        }
        return self::$ins;
    }
    protected function __construct() {
        
    }
}
class Multi extends Singleton4 {
    public function __construct() {
      
    }
}
$s6 = Singleton4::getIns();
$s7 = Singleton4::getIns();
if ($s6 === $s7) {
    echo 's6 and s7 are the same object';
} else {
    echo 's6 and s7 are NOT the same object';
}
echo '<br>';
$s8 = new Multi();
$s9 = new Multi();
if ($s8 === $s9) {
    echo 's8 and s9 are the same object';
} else {
    echo 's8 and s9 are NOT the same object';
}
在这个子类中,父构造函数的可见性变为public:
s6 and s7 are the same object s8 and s9 are NOT the same object
原因是这行代码 public function __construct() {}破坏了可见性。
所以我们需要防止子类改变父类构造函数的可见性。
防止子类重写父类构造函数
在父构造函数前面添加关键字final:
class Singleton5 {
    protected static $ins = NULL;
    public static function getIns() {
        if (self::$ins === null) {
            self::$ins = new self();
        }
        return self::$ins;
    }
    // 添加final,所以这个方法不能被覆盖!
    final protected function __construct() {
    }
}
新问题,如果类的使用者使用clone方法:
class Singleton5 {
    protected static $ins = NULL;
    public static function getIns() {
        if (self::$ins === null) {
            self::$ins = new self();
        }
        return self::$ins;
    }
    // 添加final,所以这个方法不能被覆盖!
    final protected function __construct() {
        
    }
}
$s10 = Singleton5::getIns();
$s11 = clone $s10;  // s11 克隆 s10, 新的对象创建了
if ($s10 === $s11) {
    echo 's10 and s11 are the same object';
} else {
    echo 's10 and s11 are NOT the same object';
}
结果 s10 and s11 are NOT the same object又有了多个实例
禁止使用_clone()魔术函数
class Singleton6 {
    protected static $ins = NULL;
    public static function getIns() {
        if (self::$ins === null) {
            self::$ins = new self();
        }
        return self::$ins;
    }
    // 添加final,所以这个方法不能被覆盖!
    final protected function __construct() {
    }
   
    // 禁止克隆
    final protected function __clone(){
 
    }
}
$s10 = Singleton6::getIns();
$s11 = clone $s10;  // Fatal error: Call to protected Singleton6::__clone()
这就是我们最后完成了单例模式!

