天天看点

PHP设计模式之单例模式(Singleton)

文章转载出处:http://blog.samoay.me/post/view/13

单例模式又称为单件模式,顾名思义,就是保持一个对象始终只存在一个实例(PHP本身不支持多线程<不考虑一些特殊的伪多线程实现>,因此对于PHP来说不存在多线程的安全问题,参考Java单例模式的实现),并且为该唯一单例提供一个全局访问点(一般是一个静态的getInstance方法)。单例模式应用场景非常广泛,例如:数据库操作对象、日志写入对象、全局配置解析对象等,这些场景的共同特征是从业务逻辑上来看运行期间该对象确实只需要一个实例、不断new多个实例会增加不必要的资源消耗、全局调用便利。下面分别说一下这三方面的理解:

业务上只需要一个实例

我先举一个反例,比如做一个购物网站,存在许多商品,这些商品各种属性值都不一样(如商品ID、商品名称、商品价格等等)。这个时候需要显示一个商品列表,假如我们建立一个类

Product

作为数据映射对象,那么从业务需求上来说,一个实例就无法满足业务需求,因为每个商品都不一样。反之,以数据库连接对象为例,假如还是以这个购物网站为例,有一个MySQL数据库

127.0.0.1:3306

,那么在一个进程中无论我们需要进行多少次针对该数据库的操作,都只需要连接数据库一次,使用同一个数据库连接句柄(MySQL Connection Resource),从业务需求上来看就只需要一个实例。

不断new操作增加不必要的资源消耗

姑且不考虑PHP底层对象新建对象相关的逻辑运算和内存方面的资源消耗,我们一般会在类的构造方法(new操作肯定会调用)中进行一些业务操作,例如数据库连接对象可能会在构造方法中尝试读取数据库配置并进行数据库连接(如mysqli::__construct())、日志写入对象可能会判断日志写入目录是否存在并可写入(不存在可能还会尝试创建该目录)、全局配置解析对象可能需要定位配置文件的保存目录并进行文件扫描等等。这些业务操作都会消耗相当的资源,如果在一个进程中我们只需要做一次,将会非常有利于提高应用的运行效率。

全局调用便利

因为单例模式的一大特点就是通过静态方法来获取对象实例,那么这就意味着访问对象的方法时不需要先new一个对象的实例,如果该对象需要在很多地方使用,则提高了调用的便利性。

下面通过一个日志操作类来举例说明:

class Logger{

    //首先,需要一个私有的静态变量来存储产生的对象实例

    private static $instance;

    //业务变量,保存日志写入路径

    private $logDir;

    //构造方法,注意必须也是私有的,不允许被外部实例化(即在外部被new)

    private function __construct(){

        //调试输出,测试对象被new的次数

        echo "new Logger instance\r\n";

        $logDir = sys_get_temp_dir(). DIRECTORY_SEPARATOR . "logs";

        if (!is_dir($logDir) || !file_exists($logDir)){

            @mkdir($logDir);

        }

        $this->logDir = $logDir;

    }

    //类唯一实例的全局访问点,用于判断并返回对象实例,供外部调用

    public static function getInstance(){

        if (is_null(self::$instance)){

            $class = __CLASS__;//获得本对象的类名,也可以用 new self() 方式

            self::$instance = new $class();

        }

        return self::$instance;

    }

    //重载__clone方法,不允许对象实例被克隆

    public function __clone(){

        throw new Exception("Singleton Class Can Not Be Cloned");

    }

    //具体的业务方法,实际可以有很多方法,示例简略

    public function logError($message){

        $logFile = $this->logDir . DIRECTORY_SEPARATOR . "error.log";

        error_log($message, 3, $logFile);

    }

}

//日志调用

$logger = Logger::getInstance();

$logger->logError("An error occured");

$logger->logError("Another error occured");

//或者这样调用

Logger::getInstance()->logError("Still have error");

Logger::getInstance()->logError("I should fix it");

在单例模式中可能遇到一种比较特殊的情况,比如数据库连接对象,对于大型应用来说,很可能需要连接多台数据库,那么不同的数据库公用一个对象可能会产生问题,比如连接的分配、获取insert_id,last_error等。这个问题也比较好解决,就是把我们的$instance变量变成一个关联数组,通过给getInstance方法传入不同的参数获取不同的"单例对象"(引号的含义是:严格来说类可能被new多次,但是这个new也是在我们的控制之内的,而不是在类外部):

class MysqlServer{

    //注意,变成复数了哦^_^  当然只是为了标识而已

    private static $instances = array();

    //业务变量,保持当前实例的mysqli对象

    private $conn;

    //显著特征:私有的构造方法,避免在类外部被实例化

    private function __construct($host, $username, $password, $dbname, $port){

        $this->conn = new mysqli($host, $username, $password, $dbname, $port);

    }

    //类唯一实例的全局访问点

    public static function getInstance($host='localhost', $username='root', $password='123456', $dbname='mydb', $port='3306'){

        $key = "{$host}:{$port}:{$username}:{$dbname}";

        if (empty(self::$instances[$key])){

            //这里也可以用 new self(); 的方式

            $class = __CLASS__;

            self::$instances[$key] = new $class($host, $username, $password, $dbname, $port);

        }

        return self::$instances[$key];

    }

    //重载__clone方法,不允许对象实例被克隆

    public function __clone(){

        throw new Exception("Singleton Class Can Not Be Cloned");

    }

    //查询业务方法,后面省略其它业务方法

    public function query($sql){

        return $this->conn->query($sql);

    }

    //尽早释放资源

    public function __destruct(){

        $this->conn->close();

    }

}

  • 问题1:单例类能否拥有子类,因为单例类的构造方法是私有的,因此无法被继承,如果要继承则需要将构造方法改为protected或public,这就违背了单例模式的本意。因此,如果你想给单例类加子类,那就需要回头想想是否错用了模式,或者结构设计上有问题。
  • 问题2:单例滥用,单例模式相对来说比较好理解和实现,因此一旦认识到单例模式的好处,很可能什么类都想写成单例,因此在使用次模式之前一定要考虑上述3种情况,看是否真的有必要使用。

继续阅读