单例模式,就是保持一个对象只存在一个实例。并且为该唯一实例提供一个全局的访问点(一般是一个静态的getInstance方法)。单例模式应用场景非常广泛,例如:数据库操作对象
日志写入对象
全局配置解析对象
这些场景的共同特征是从业务逻辑上来看运行期间改对象却是只有一个实例
不断new多个实例会增加不必要的资源消耗
全局调用便利
下面分别说明这三个方面:
1. 业务上只需要一个实例
以数据库连接对象为例,加入有一个购物网站,有一个MySQL数据库127.0.0.1:3306
,那么在一个进程中无论我们需要进行多少次针对改数据库的操作,都只需要连接数据库一次,使用相同的数据库连接句柄(MySQL Connection Resource),从业务需求上来看就只需要一个实例。
相反,同样以购物网站为例,存在许多商品,这些商品都不一样(id,name,price..),这个时候需要显示一个商品列表,加入我们建立一个Product
作为数据映射对象,那么从业务需求上来说,一个实例就无法满足业务需求,因为每个商品都不一样。
2. 不断new操作增加不必要的资源消耗
我们一般会在类的构造方法(new操作肯定会调用)中进行一些业务操作,例如数据库连接对象可能会在构造方法中尝试读取数据库配置并进行数据库连接(如mysqli::__construct())、日志写入对象会判断日志写入目录是否存在并写入(不存在可能尝试创建改目录)、全局配置解析对象可能需要定位配置文件的保存目录并进行文件扫描等。
这些业务都会消耗相当的资源,如果在一个进程中我们值需要做一次,将会非常有利于我们提高应用的运行效率。
3. 全局调用便利
因为单例模式的一大特点就是通过静态方法获取对象实例,那么就意味着访问对象的方法时不需要先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_idlast_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种情况,看是否真的有必要使用。