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