代理模式屬於結構性設計模式,針對類別與物件組合在一起的經典結構。代理模式也是一種使用較多的設計模式,需要我們重點掌握,他可以在不改變目標物件的情況下,加入一些額外的功能。
定義
代理模式(Proxy)為其他物件提供一種代理以控制對這個物件的存取。使用代理模式建立代理對象,讓代理對象控制目標對象的存取(目標對象可以是遠端的對象、建立開銷大的對像或需要安全控制的對象),並且可以在不改變目標對象的情況下添加一些額外的功能。
問題
目前系統關於使用者登入註冊的業務,有一個Login類別。偽代碼如下:
class UserLogin { // …… 省略属性和部分方法 public function login ($name, $pass) { // 登录业务 } public function reg ($name, $pass) { // 注册业务 } }
現在,我們想在使用者登入和註冊的業務中加入一個功能-限流,讓客戶端呼叫該方法的頻次限制在一秒鐘最多5次。現在,我們來實現該功能,偽代碼如下:
class UserLogin { // …… 省略属性和部分方法 public function login ($name, $pass) { // 限流 $limit = new Limit(); if ($limit->restrict()) { // …… } // 登录业务 } public function reg ($name, $pass) { // 限流 $limit = new Limit(); if ($limit->restrict()) { // …… } // 注册业务 } }
大家看看上面的程式碼,它有幾個問題,首先,限流程式碼侵入到業務程式碼中,跟業務程式碼高度耦合。其次,限流和業務代碼無關,違反單一職責原則。
實作
接下來,我們用代理模式重寫上面的程式碼,重寫後的程式碼如下所示:
interface IUserLogin { function login (); function register (); } class UserLogin implements IUserLogin { // …… 省略属性和部分方法 public function reg ($uname, $pass) { // 注册业务 } public function login ($uname, $pass) { // 登录业务 } } class UserLoginProxy implements IUserLogin { 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()) { // ... } return $this->login->login($uname, $pass); } public function register($uname, $pass) { if ($this->limit->restrict()) { // ... } return $this->login->register($uname, $pass); } }
上面的方法是基於接口而非實現程式設計的設計思想,但如果原始類別並沒有定義接口,或者這個類別並不是我們開發和維護的,那麼要怎麼實現代理模式呢?
對於這種外部類別的擴展,我們一般會採用繼承的方法來實作。
class UserLogin { public function reg ($uname, $pass) { // 注册业务 } public function login ($uname, $pass) { // 登录业务 } } class UserLoginProxy 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()) { // ... } return parent::login($uname, $pass); } public function reg($uname, $pass) { if ($this->limit->restrict()) { // ... } return parent::register($uname, $pass); } }
大家看看上面的程式碼,是不是還有什麼問題?你會發現
if ($this->limit->restrict()) { // ... }
這段相似的程式碼,出現了兩次。現在我們只是為兩個方法增加了限流功能,如果UserLogin類別有10個方法,每個方法我們都想要新增限流的功能,那麼我們就需要重複複製10次該段程式碼。如果,我們想要為10給類別中所有方法都加入限流功能,每個類別中都有10個方法,那麼上面的限流程式碼將會重複100次。
當然,你會說我可以將限流的程式碼封裝到一個函數裡不就解決了上述問題麼?但還有一個問題解決不了,原始類別裡每個方法在代理類別中都要重新實作一遍。就像上面原始類別裡有reg、login方法,代理類別裡也有reg、login方法。
動態代理程式
如何解決上述的問題,我們可以藉助動態代理來解決。想要使用動態代理,就要理解並使用PHP中的反射機制。
php具有完整的反射 API,增加了對類別、介面、函數、方法和擴充進行反向工程的能力。此外,反射 API 提供了方法來取出函數、類別和方法中的文件註解。關於php的反射相關知識,這裡就不詳述了,大家可以自行查閱相關資訊。
注意,使用反射對效能消耗很大,一般情況下請不要使用。
下面我們來展示如何用反射實作動態代理,偽代碼如下:
class UserLogin { public function reg ($uname, $pass) { // 注册业务 echo '注册业务' . PHP_EOL; } public function login ($uname, $pass) { // 登录业务 echo '登录业务' . PHP_EOL; } } class LimitProxy { // 用来保存多个实例对象 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 LimitProxy($login); $loginProxy->reg('gwx', '111111'); $loginProxy->login('james', '111111111');
應用程式場景
存取控制(保護代理程式)。例如係統有一個訂單的模組,原本該模組也有權限控制,但現在我們希望只針對指定ip的客戶端可以訪問,那麼我們可以使用代理模式。
本機執行遠端服務 (遠端代理)適用於服務物件位於遠端伺服器上的情況。
在業務程式碼中開發一些非功能性的需求,例如:限流、統計、日誌記錄
快取方面的應用,例如新增一個快取代理,當快取存在時,就呼叫快取代理來取得快取的數據,當快取不存在時,就呼叫原始介面。
以上是一文讀懂php設計模式之代理模式的詳細內容。更多資訊請關注PHP中文網其他相關文章!