---
layout: post
title: PHP单例模式继承体系结构设计及封装php数据库medoo
description: 如果一个类的对象实例在整个程序运行过程中只需创建一个,则可以将这个类设计为单例类,以避免多次创建所带来的系统额外开销。先来回顾一下单例模式的实现,PHP单例模式继承体系结构设计及封装php数据库medoo现
keywords: php,PHP单例,单例模式,Singleton Pattern
author: admin
date: 2024-01-27 23:12
category: 网络技术
tags: 单例
---

**单例模式(Singleton Pattern)**:顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在一个系统中,如果一个类的对象实例在整个程序运行过程中只需创建一个,则可以将这个类设计为单例类,以避免多次创建所带来的系统额外开销。先来回顾一下单例模式的实现。

## PHP中单例模式的实现

单例模式的实现

- ①、私有静态属性,用来储存生成的唯一实例
- ②、私有构造函数, 用来创建并只允许创建一个实例
- ③、私有克隆函数,防止克隆——clone
- ④、公共静态方法,用来访问静态属性储存的对象,如果没有对象,则生成此单例


单例模式,实际上是通过类的静态成员属性来实现的:

```
class Singleton{
private static $instance = null;

private function __construct(){}
private function __clone(){}

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

$s = Singleton::getInstance();
```

如上所示,使Singleton类构造方法私有,Singleton类则无法通过new关键字实例化。而公有静态方法getInstance则在类内部调用构造方法对本类实例化,同时用私有静态属性$instance引用该实例。这样一来,由于静态成员属性对于类来说只有一个,则Singleton类在整个程序中则只会被实例化一次。注意使__clone方法私有,防止对象被复制。此外,可以通过getInstance函数传参给构造函数,完成对对象的初始化。

然而在系统中,如果只有一个类设计成单例,那上述代码工作起来倒也没有什么问题。但倘若有多个类都要做成单例,那么如果每个类都编写相同的getInstance方法则降低了封装,产生大量的代码重复。这时可以通过类的继承来解决这个问题。

简单说,一般实例都是通过new来创建对象实例, **单例模式** 也用new创建对象,只是换个地方而已,从类外到类内。构造函数被申明为private或者protected这注定无法通过new的方法创建实例对象了。一般最常用的地方是数据库连接、缓存操作、分布式存储。

## 继承单例类实现各子类的单例

为了便于理解,我们举一些实际的例子来说明如何继承一个单例类使子类都实现单例模式。
假设定义一个“神仙”类,再定义一个“悟空”类和“八戒”类。毫无疑问世上只有一个悟空和一个八戒,他们应该是单例,并且都继承自神仙。按照前面介绍的单例模式实现,我们可以这样编写代码:

```
class ShenXian{
protected static $instance = null;
protected $name;

protected function __construct(){
echo '神仙构造了..';
$this->name = '神仙';
}
protected function __clone(){}

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

public function getName(){
return $this->name;
}
}

class Wukong extends ShenXian{
protected function __construct(){
echo '悟空构造了..';
$this->name = '齐天大圣';
}
}

class BaJie extends ShenXian{
protected function __construct(){
echo '八戒构造了..';
$this->name = '天蓬元帅';
}
}

$wukong = WuKong::getInstance();
$bajie = BaJie::getInstance();

echo $wukong->getName().' '.$bajie->getName();

```

首先神仙类作为父类依照前面提到的单例类实现方式,限定自己为单例类。同时声明了名字属性(name)。子类仅仅对构造函数进行重写。
但遗憾的是,上面的代码并没有像我们想象中的那样工作。
程序运行的结果是这样的:

神仙构造了..神仙 神仙

这样的结果说明了两个问题:

1. 虽然有三个类,但真正实例化的即构造函数被执行的只有ShenXian类(虽然我们分别调用的是Wukong类和BaJie类的getInstance方法)。
2. 变量$wukong和$bajie获得的都是ShenXian类的实例的引用。

第一个问题出现的原因在于父类的getInstance静态方法。
在getInstance方法中,由于是在类体内,self关键字指代的是该类自己即ShenXian类。虽然子类(WuKong、BaJie)继承了父类的getInstance方法,但self所指代的类是在php文件编译过程中已经决定了的。所以从始至终,self指代的都是ShenXian类,而从没有指代过WuKong类或者BaJie类。
为了解决这个问题,php推出了一种名为后期静态绑定的特性。通过使用static关键字而不是self,实现在执行时确定static所指代的类究竟是谁。所以getInstance函数需要修改为这样:

```
class ShenXian{
...

public static function getInstance(){
if(static::$instance === null){
static::$instance = new static();
}
return static::$instance;
}

...
}
```

上述代码中使用static关键字在功能上和self是一样的,它们都是在类体内调用类的方式,区别在于self关键字是在编译时决定它所指代的类,它写在了哪个类中,它指代的就是那个类。而static关键字则是在执行的过程中才决定它所指代的类。采用static关键字后,虽然方法声明在ShenXian类中,但由于继承关系,调用它的是子类(WuKong、BaJie),则static所指代的则分别是两个子类了。这样在getInstance方法中实例化的,或者说调用的就变成了子类构造函数了。
修改代码后程序的执行结果:

悟空构造了..齐天大圣 齐天大圣

虽然悟空构造了,但八戒没有构造,而且变量$bajie引用的实例却也成为了悟空。但好在离正确结果近了一步,第一个问题已经解决了。
出现这样运行结果的原因在于受保护成员属性$instance上,也是前文提到的第二个问题。再回过头来看getInstance函数,由于操作的是一个静态成员属性,要想对属性进行访问,需要在类体外用类名,在类体内使用self或static关键字。对于同一个类的多个对象,它的非静态属性随着对象的创建而创建不同的副本,所以每个对象的非静态成员属性都不尽相同。但静态属性属于类的属性,一个类的所有对象都共享这一个静态属性。简言之,一个静态属性仅属于一个类。在上例中,定义了$instance静态属性的只有ShenXian类。那么即使在getInstance方法中,采用static关键字访问的$instance属性,由于子类并没有重新声明,也仅仅是继承自父类的静态属性,故使用static访问的也是父类(ShenXian)的静态成员属性$instance。

至此问题也就清晰了:单例类通过类(静态)属性来缓存本类唯一的实例,在上述代码继承体系中,子类的实例绑定到了父类的静态属性$instance上,正因为如此,首先声明的$wukong变量创建的是WuKong实例,但随后$bajie变量通过getInstance方法则并没有再次创建对象,因为父类的$instance属性此时已经不为null,故返回的仍是WuKong类实例。

解决这个问题的方法就是在每个子类中声明静态属性$instance即可。子类声明$instance静态属性,它们从属于各个子类,在父类的getIntance方法中,通过static关键字,调用子类并实例化,然后又赋给子类静态属性$instance。这样就可以实现将获取单例的函数封装在父类的目的。修改子类代码如下:

```
class WuKong extends ShenXian{
protected static $instance = null;

...
}

class BaJie extends ShenXian{
protected static $instance = null;

....
}
```

修改后,程序的执行结果是这样的:

悟空构造了..八戒构造了..齐天大圣 天蓬元帅

## 最后还有哪些问题?

写到这里,看起来单例类的继承体系结构工作起来已经没有什么问题了。但仍可以做一些修饰和润色。

1. 父类ShenXian类看起来不应该能被实例化,abstract一下比较好。
2. 父类getInstance函数如果被覆盖就糟了,final一下比较好。
3. 由于子类可以重写构造函数__construct和克隆函数__clone并调整其访问权限为public,为避免破坏单例模式,所以也final一下比较好。
4. 构造函数被final修饰后看起来无法在子类对对象进行初始化了,可以考虑这样做:在构造函数中调用一个名为init的函数,当子类想要初始化对象时可以在子类重写init,交给父类进行调用。但是要在父类中定义声明函数init,可以为一个空函数,目的是防止子类不重写该方法导致抛出找不到方法报错。
5. 如果想要强制子类实现init方法也可以将其声明为abstract抽象方法。
6. 父类的静态成员属性$instance看起来已经没有什么用了,可以考虑不做声明。这样做也将保存本类单例实例的职责交给了子类及其静态属性$instance上。当子类不声明$instance时会出现报错的问题(访问了不存在的静态属性),所以这可以约束子类必须声明$instance,同时防止直接通过父类调用getInstance方法。

最后贴出父类代码:

```
abstract class Singleton{
final protected function __construct(){
$this->init();
}

final protected function __clone(){}
protected function init(){}
//abstract protected function init();

public static function getInstance(){
if(static::$instance === null){
static::$instance = new static();
}
return static::$instance;
}
}
```

## 数据库Medoo使用php单例模式

```
use Medoo\Medoo;
class HigridModel extends Medoo{
//私有的静态属性,用于存储类对象
private static $_instance = null;
//私有的构造方法,保证不允许在类外 new
private function __construct()
{
$options = [
'type' => 'mysql',
'host' => DB_HOST,
'database' => DB_NAME,
'username' => DB_USER,
'password' => DB_PASS,
'prefix' => ' higrid_net_'
];
parent::__construct($options);
}
//私有的克隆方法, 确保不允许通过在类外 clone 来创建新对象 前面增加final报错
private function __clone(){
trigger_error('higrid.net Clone is not allow !',E_USER_ERROR);
}
//公有的静态方法,用来实例化唯一当前类对象 higrid.net
public static function getInstance(){
if(self::$_instance === null){
self::$_instance = new self;
}
return self::$_instance;
}
}

```

## php单例模式封装数据库Medoo
在其他地方使用时,采用php封装数据库Medoo进行查询。

```
$db = HigridModel::getInstance();
$datas = $db->select('users','*',[...]);

```