本文归于合集:面向对象和设计模式
一、代理模式的使用场景
代理模式可以在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加额外功能。
直接上例子,假如我们要给用户模块的登录和注册接口添加一个性能告警的小功能,这个功能要求我们计算接口的耗时,再根据耗时是否有超过阈值决定是否告警。按照普通的实现思路,我们也许会直接把这段代码加到 注册和登录 方法内部:
<?php
class UserController { //...省略其他属性和方法... private $metricsCollector; // 性能计数器,假设已经在构造函数那里将其注入
public function login($telephone, $password) { $startTimestamp = microtime(true);
// ... 省略login逻辑...
$endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp; // 接口耗时 $requestInfo = new RequestInfo("login", $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); // 记录接口耗时
//...返回User数据... }
public function register($telephone, $password) { $startTimestamp = microtime(true);
// ... 省略register逻辑...
$endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp; $requestInfo = new RequestInfo("register", $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo);
//...返回User数据... }}
上面写法的问题在于 收集接口请求信息(如耗时、接口名,开始时间等)的代码应该属于一种公共功能,跟用户模块的业务代码无关,不应该放到一个UserController类中。业务类最好是职责更加单一,只聚焦业务处理。
为了将公共功能代码和业务代码解耦,或者说我们想在不入侵旧有类的业务代码情况下添加一些额外功能,此时就可以使用代理模式 。具体做法是创建一个代理类 UserControllerProxy ,它和原始类 UserController 实现相同的接口 IUserController,计算耗时和记录接口信息的工作交给 代理类 完成,被代理类只负责用户相关的业务逻辑,代理类委托被代理类调用业务逻辑的方法。
<?php
interface IUserController { public function login($telephone, $password); public function register($telephone, $password);}
class UserController implements IUserController { //...省略其他属性和方法...
public function login($telephone, $password) { //...省略login逻辑... }
public function register($telephone, $password) { //...省略register逻辑... }}
class UserControllerProxy implements IUserController { private $metricsCollector; private $userController;
public function __construct(UserController $userController) { $this->userController = $userController; $this->metricsCollector = new MetricsCollector(); }
public function login($telephone, $password) { $startTimestamp = microtime(true);
// 委托 $this->userController->login($telephone, $password);
$endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp; $requestInfo = new RequestInfo("login", $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); }
public function register($telephone, $password) { $startTimestamp = microtime(true); $this->userController->register(telephone, password);
$endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp; $requestInfo = new RequestInfo("register", $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); }}
//UserControllerProxy使用举例//因为原始类和代理类实现相同的接口,是基于接口而非实现编程//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码$userController = new UserControllerProxy(new UserController());
为了尽量少改动调用处(只能做到少改动调用处,无法做到不改动调用处),我们需要让代理类实现和被代理类相同的接口。
在上面这个例子中,我们采用了 组合 的方式,让代理类组合被代理类,既保留了UserController的业务逻辑功能,又增加了计算接口耗时和监控报警的额外功能。
除了采用 组合 这个方式实现代理模式,还可以采用继承的方式实现代理模式,让代理类继承自被代理类,然后重写被代理类的 register() 和 login()方法。
class UserControllerProxy extends UserController { private $metricsCollector;
public function __construct() { $this->metricsCollector = new MetricsCollector(); }
public function login($telephone, $password) { $startTimestamp = microtime(true);
// 委托 parent::login($telephone, $password);
$endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp; $requestInfo = new RequestInfo("login", $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); }
public function register($telephone, $password) { // ...省略 }}
代理模式的角色和类图如下:
Subject:抽象主题角色。是定义"操作/行为"的接口类或抽象类;
RealSubject:具体主题角色。是被代理角色,是业务逻辑的具体执行者;
ProxySubject:代理主题角色。是代理角色,它负责被代理类的委托调用,并在处理前后做预处理和善后处理工作。
二、动态代理
上面的代码其实还是有比较明显的缺陷的,那就是 login 和 register 方法中重复实现了计算接口耗时的逻辑。
如果 UserController 中除了login和register之外的其他方法也要加上计算接口耗时的逻辑呢,我们难道要在代理类的这些方法里面都加上这些逻辑?
如果除了UserController类之外,还有OrderController类也需要加上这个计算耗时的功能呢,难道我还要再为 OrderController 创建一个 OrderControllerProxy 类,那岂不是会导致类的数量成倍增加?
为了解决这个问题,我们需要对上面的代理模式代码做一些改动。
a. 为了避免为每一个业务类都创建一个代理类,我们不再使用继承的方式实现代理模式,只能用组合的方式,只为所有需要计算接口耗时的被代理类创建一个公共的代理类。
b. 这个公共的代理类里面不重写代理类的任何方法,而是在__call魔术方法中委托调用被代理类的原方法。
PS: __call() 魔术方法的作用是当外部调用者调用了对象的一个不可访问方法(不可访问是指方法是私有的或者方法不存在)时,__call()就会自动触发。
class metricsAbilityProxy{ // 类名的意思是"增加性能统计能力的代理类" private $metricsCollector; private $proxiedObject; // 被代理类
public function __construct($proxiedObject) { $this->metricsCollector = new MetricsCollector(); $this->proxiedObject = $proxiedObject; }
public function __call($method, $args){ // $method是外部调用不存在方法的方法名,$args是调用不存在方法的参数 if (!method_exists($this->proxiedObject, $method)) // 如果被代理类不存在这个方法则报错 throw new Exception("method not existed");
// 下面是接口耗时逻辑 和 统计告警逻辑 $startTimestamp = microtime(true); call_user_func_array([$this->proxiedObject, $method], $args); // 调用被代理类的业务逻辑方法 $endTimeStamp = microtime(true); $responseTime = $endTimeStamp - $startTimestamp;
$requestInfo = new RequestInfo($method, $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); }}
// 使用示例$userControllerProxy = new metricsAbilityProxy(new UserController());$userControllerProxy->register('zbp', '123456');$userControllerProxy->login('zbp', '123456');
现在看上去好像已经没有什么问题了对吗?对于接口耗时和告警这个功能而言,是的。但是我如果希望能为 被代理类 添加不只一种额外功能呢?
例如,我还希望为 UserController 类增加一个 "访问限流" 的功能,现在UserController就有两种额外功能附加了。而OrderController类我不想他增加"访问限流"功能,而是增加一个"记录每日客户访问热度的日志信息"的额外功能。
这个需求要怎么改装上面的Proxy类才能满足呢?
我们需要将额外功能从 Proxy 类上拆分,每一个额外功能分别放到一个类中实现,这样符合单一职责原则。而Proxy类需要设置一个数组存放被代理类具体需要的额外功能类,在__call中组装和调用这些额外功能类即可。
如下所示: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
<?php
interface extraAbility { public function beforeAction(); public function afterAction();} abstract class abstractExtraAbility implements{ private $proxy; // 依赖注入代理类 public function __construct(AbilityAppendProxy $proxy){ $this->proxy = $proxy; $this->init(); } protected function init(){} // 可重写 public function beforeAction(){} public function afterAction(){}} class metricsAbility extends abstractExtraAbility{ protected $startTime; protected $endTime; private $metricsCollector; protected function init(){ $this->metricsCollector = new MetricsCollector(); } public function beforeAction(){ $startTime = microtime(true); } public function afterAction(){ $responseTime = $endTimeStamp - $startTimestamp; $requestInfo = new RequestInfo($this->proxy->getMethodRunned(), $responseTime, $startTimestamp); $this->metricsCollector->recordRequest($requestInfo); }} class LimitRequestAbility extends abstractExtraAbility{ // 限流能力 // ...省略} class AbilityAppendProxy{ // 代理类 private $extraAbilities = []; // 存储 extraAbility 对象 private $proxiedObject; // 被代理类 private $methodRunned; private $methodRes; // 方法运行的返回结果 private $extraDataMap; // 用来存储 extraAbility 对象产生的中间结果,这里是考虑到多个 extraAbility 对象可以有数据上的依赖关系,可通过Proxy对象的这个属性在各个extraAbility间传递数据。 public function __construct($proxiedObject, $extraAbilityClasses=[]) { $this->proxiedObject = $proxiedObject; foreach($extraAbilityClasses as $extraAbilityClass){ $extraAbility = new $extraAbilityClass($this); // 这里是在 Proxy 类中对 extraAbility 类进行实例化,有点违反依赖注入原则。可以考虑优化成在专门生成Proxy对象的工厂方法中对 extraAbility类实例化再注入进来 $this->extraAbilities[] = $extraAbility; } } public function getMethodRunned(){ return $this->methodRunned; } public function getMethodRes(){ return $this->methodRes; } public function __call($method, $args){ // $method是外部调用不存在方法的方法名,$args是调用不存在方法的参数 if (!method_exists($this->proxiedObject, $method)) // 如果被代理类不存在这个方法则报错 throw new Exception("method not existed"); // 下面是接口耗时逻辑 和 统计告警逻辑 $this->methodRunned = $method; /** @var abstractExtraAbility $extraAbility */ foreach($this->extraAbilities as $extraAbility){ $extraAbility->beforeAction(); } $this->methodRes = call_user_func_array([$this->proxiedObject, $method], $args); // 调用被代理类的业务逻辑方法 foreach($this->extraAbilities as $extraAbility){ $extraAbility->afterAction(); } }}$userControllerProxy = new AbilityAppendProxy(new UserController(), [metricsAbility::class, LimitRequestAbility::class]);$userControllerProxy->login('zbp', '123456');
上面的代码示例基本上已经能够解决之前所提到的问题。但是还有一点缺陷,那就是 Proxy 对象是接收了一个存储 extraAbility 类的数组,然后 extraAbility 对象的实例化是由 Proxy 类完成的,如果extraAbility内部的处理逻辑需要依赖外界的一些数据,那么我们是无法将这些数据通过Proxy类传递给 extraAbility对象。
举个例子:假设我们要开发一个接口,接口查询到的数据要缓存,如果接口的请求参数相同,则以请求参数作为key建设缓存。在设定的过期时间内,如果查询的是相同请求参数的数据则直接返回缓存结果,而不用重新进行逻辑处理。要求通过在代理类中添加缓存功能 CacheAbility 来完成。
CacheAbility 对象要想完成这个任务,就得拿到用户的请求参数 params,而params就必须得由Proxy类之外的上传调用传入。
而且除了 CacheAbility,其他的 extraAbility 对象可能也需要依赖各种各样的数据,每种 extraAbility 需要什么样的数据,这是Proxy类无法知道的,只有上层调用(业务层)能够知道。
此时有两个解决办法:
第一个解决方法是将上面代码中 Proxy 类构造函数的第二参从 $extraAbilityClasses 改为 $extraAbilityObjects,也就是从传入extraAbility类名改为传入有效状态的extraAbility对象,这些对象已经在Proxy类之外的上传调用者中实例化和初始化好了的。如果不希望业务层去做 extraAbility 对象的实例化和初始化工作,也可以交给工厂类或工厂方法去做,业务层调工厂类拿到这些有效状态的对象就好。
第二个解决方法是将上面代码中 Proxy 类构造函数的第二参从 $extraAbilityClasses 改为 $extraAbilityConfigs,$extraAbilityConfigs是个二维数组,包含了 extraAbility类名和它相对应的初始化方法所需的参数,然后 extraAbility 对象的实例化和初始化依旧由 Proxy类内部负责。
第三个方法是兼容前两个方法, Proxy 类构造函数的第二参既支持其内的数组元素是一个有效状态的 extraAbility对象,也支持数组元素是一个 extraAbilityConfig 配置。不过这样的话 Proxy类就需要在__construct中实现更多和 extraAbility类相关的判断和处理逻辑。
最后总结:
代理模式最常用的一个应用场景就是,在业务系统中开发一些非业务的通用功能,比如:监控、统计、鉴权、限流、事务、幂等、日志 和 缓存。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让程序员只需关注业务方面的开发。
其实也不一定只适用于添加非业务的通用功能,业务功能也可以,只要添加的是共用性的逻辑都可以用代理模式。