PHP设计模式之代理模式

定义

代理模式(Proxy)为其他对象提供一种代理以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制目标对象的访问(目标对象可以是远程的对象、创建开销大的对象或需要安全控制的对象),并且可以在不改变目标对象的情况下添加一些额外的功能。

问题

目前系统有一个Login类,完成用户的注册及登录的功能。现在我们想往登录及注册的方法中加入限流的功能——每秒种内调用该方法的次数不能超过3次。

class Login
{
   public function register ($uname, $pass)
  {
// 注册业务
  }

   public function login ($uname, $pass)
  {
       // 登录业务
  }
}

限流的功能我们有一个专门的类Limit,如果我们不使用代理模式,直接修改原始类,那么就会像下面这样

class Login
{
   public function register ($uname, $pass)
  {
       // 限流
       $limit = new Limit();
       if ($limit->restrict()) {
           // ...
      }
// 注册业务
  }

   public function login ($uname, $pass)
  {
        // 限流
       $limit = new Limit();
       if ($limit->restrict()) {
           // ...
      }
       // 登录业务
  }
}

上面的代码有几个问题,首先,限流代码侵入到业务代码中,跟业务代码高度耦合。其次,限流和业务代码无关,违背单一职责原则。

现在我们修改上面的代码,改成用代理模式来实现。首先,定义一个接口,让代理类LoginProxy及Login实现相同的接口。

interface ILogin
{
   function login ();
   function register ();
}

class Login implements ILogin
{
   public function register ($uname, $pass)
  {
       // 注册业务
  }

   public function login ($uname, $pass)
  {
       // 登录业务
  }
}

class LoginProxy implements ILogin
{
   private $limit = null;
   private $login = null;

   public function __construct(Limit $limit, Login $login)
  {
       $this->limit = $limit;
       $this->login = $login;
  }

   public function login($uname, $pass)
  {
       if ($this->limit->restrict()) {
           // ...
      }

       $this->login->login($uname, $pass);
  }

   public function register($uname, $pass)
  {
       if ($this->limit->restrict()) {
           // ...
      }

       $this->login->register($uname, $pass);
  }
}

上面的方法是基于接口而非实现编程的设计思想,但如果原始类并没有定义接口,或者这个类并不是我们开发和维护的,那么要怎么实现代理模式呢?

对于这种外部类的扩展,我们一般采用继承的方法来实现。

class Login
{
   public function register ($uname, $pass)
  {
       // 注册业务
  }

   public function login ($uname, $pass)
  {
       // 登录业务
  }
}

class LoginProxy extends Login
{
   private $limit = null;

   public function __construct(Limit $limit, Login $login)
  {
       $this->limit = $limit;
       $this->login = $login;
  }

   public function login($uname, $pass)
  {
       if ($this->limit->restrict()) {
           // ...
      }

       parent::login($uname, $pass);
  }

   public function register($uname, $pass)
  {
       if ($this->limit->restrict()) {
           // ...
      }

       parent::register($uname, $pass);
  }
}

上面的代码还是有问题的。一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。

这个问题,我们可以通过动态代理来解决。

反射

如想使用动态代理,我们首先要知道使用反射。 php具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。

注意,使用反射对性能消耗很大,一般情况下请不要使用。

下面我们来看一个实例,通过实例来学习如何使用反射

class Person{

   public $name, $age, $sex;

   static function show($name, $age, $sex='男'){
       echo "姓名:$name,年龄:$age,性别:$sex";
  }

   function say($content){
       echo "我想说的是:$content";
  }

   function eat($food = 'apple'){

  }
}


$per = new Person();

//参数可以是类名,或者类的实例
$ref = new ReflectionClass('Person');

//获取类里面的所有方法
$class_methods = $ref->getMethods();

//是一个数组,每个对象包含了方法名和所属类
echo '<br/>';
echo "<pre>";print_r($class_methods);echo "<pre>";

//是否拥有某个方法
$has_method = $ref->hasMethod('say');

//获取某个方法的信息,第一个参数可以是类名或类的实例
$some_method = new ReflectionMethod('Person','say');

//判断是否私有,还有static,public
$some_method->isPrivate();

//方法的调用,
if ($some_method->isPublic()&&!$some_method->isAbstract()) {

   if ($some_method->isStatic()){
       //静态方法第一个参数是null,后面参数写方法的参数,可以传递一个或者多个,并且这个方法可以接受数量可变的参数。
       $some_method->invoke(null,'zhangsan','23');
  } else {
       //非静态方法第一个参数传递一个对象
       $some_method->invoke($per,'生活真好');
  }
}

动态代理

学完反射后,我们就可以来完成一个动态代理模式了。

代码如下:

class Login
{
   public function register ($uname, $pass)
  {
       // 注册业务
       echo $uname . '|' . $pass . PHP_EOL;
       echo '注册业务' . PHP_EOL;
  }

   public function login ($uname, $pass)
  {
       // 登录业务
       echo '登录业务' . PHP_EOL;
  }
}

class LoginProxy
{
   private $target = [];

   public function __construct(Object $obj)
  {
       $this->target[] = $obj;
  }

   public function __call($name, $arguments)
  {
       foreach ($this->target as $obj) {
           $ref = new \ReflectionClass($obj);

           if ($method = $ref->getMethod($name)) {
               if ($method->isPublic() && !$method->isAbstract()) {
                   // 限流
                   echo "这里是限流业务处理" . PHP_EOL;

                   $result = $method->isStatic() ? $method->invoke(null, $obj, ...$arguments) : $method->invoke($obj, ...$arguments);
                   return $result;
              }
          }
      }
  }
}

测试代码如下:

$login = new Login();
$loginProxy = new LoginProxy($login);

$loginProxy->register('gwx', '111111');
$loginProxy->login('james', '111111111');

应用场景

  • 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。
  • 本地执行远程服务 (远程代理)适用于服务对象位于远程服务器上的情形。
  • 在业务代码中开发一些非功能性的需求,比如:限流、统计、日志记录
  • 缓存方面的应用,比如添加一个缓存代理,当缓存存在时,就调用缓存代理获取缓存的数据,当缓存不存在时,就调用原始接口。