代理模式屬于結(jié)構(gòu)性設(shè)計(jì)模式,針對(duì)類與對(duì)象組合在一起的經(jīng)典結(jié)構(gòu)。代理模式也是一種使用較多的設(shè)計(jì)模式,需要我們重點(diǎn)掌握,他可以在不改變目標(biāo)對(duì)象的情況下,添加一些額外的功能。
定義
代理模式(Proxy)為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問。使用代理模式創(chuàng)建代理對(duì)象,讓代理對(duì)象控制目標(biāo)對(duì)象的訪問(目標(biāo)對(duì)象可以是遠(yuǎn)程的對(duì)象、創(chuàng)建開銷大的對(duì)象或需要安全控制的對(duì)象),并且可以在不改變目標(biāo)對(duì)象的情況下添加一些額外的功能。
問題
目前系統(tǒng)關(guān)于用戶登錄注冊(cè)的業(yè)務(wù),有一個(gè)Login類。偽代碼如下:
class UserLogin { // …… 省略屬性和部分方法 public function login ($name, $pass) { // 登錄業(yè)務(wù) } public function reg ($name, $pass) { // 注冊(cè)業(yè)務(wù) } }
現(xiàn)在,我們想在用戶登錄和注冊(cè)的業(yè)務(wù)中添加一個(gè)功能——限流,讓客戶端調(diào)用該方法的頻次限制在一秒鐘最多5次?,F(xiàn)在,我們來實(shí)現(xiàn)該功能,偽代碼如下:
class UserLogin { // …… 省略屬性和部分方法 public function login ($name, $pass) { // 限流 $limit = new Limit(); if ($limit->restrict()) { // …… } // 登錄業(yè)務(wù) } public function reg ($name, $pass) { // 限流 $limit = new Limit(); if ($limit->restrict()) { // …… } // 注冊(cè)業(yè)務(wù) } }
大家看看上面的代碼,它有幾個(gè)問題,首先,限流代碼侵入到業(yè)務(wù)代碼中,跟業(yè)務(wù)代碼高度耦合。其次,限流和業(yè)務(wù)代碼無關(guān),違背單一職責(zé)原則。
實(shí)現(xiàn)
接下來,我們用代理模式重寫上面的代碼,重寫后的代碼如下所示:
interface IUserLogin { function login (); function register (); } class UserLogin implements IUserLogin { // …… 省略屬性和部分方法 public function reg ($uname, $pass) { // 注冊(cè)業(yè)務(wù) } public function login ($uname, $pass) { // 登錄業(yè)務(wù) } } 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); } }
上面的方法是基于接口而非實(shí)現(xiàn)編程的設(shè)計(jì)思想,但如果原始類并沒有定義接口,或者這個(gè)類并不是我們開發(fā)和維護(hù)的,那么要怎么實(shí)現(xiàn)代理模式呢?
對(duì)于這種外部類的擴(kuò)展,我們一般采用繼承的方法來實(shí)現(xiàn)。
class UserLogin { public function reg ($uname, $pass) { // 注冊(cè)業(yè)務(wù) } public function login ($uname, $pass) { // 登錄業(yè)務(wù) } } 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); } }
大家看看上面的代碼,是不是還有什么問題?你會(huì)發(fā)現(xiàn)
if ($this->limit->restrict()) { // ... }
這段相似的代碼,出現(xiàn)了兩次。現(xiàn)在我們只是給兩個(gè)方法添加了限流功能,如果UserLogin類有10個(gè)方法,每個(gè)方法我們都想要添加限流的功能,那么我們就需要重復(fù)復(fù)制10次該段代碼。如果,我們想要給10給類中所有方法都添加限流功能,每個(gè)類中都有10個(gè)方法,那么上面的限流代碼將會(huì)重復(fù)100次。
當(dāng)然,你會(huì)說我可以將限流的代碼封裝到一個(gè)函數(shù)里不就解決了上述問題么?但還有一個(gè)問題解決不了,原始類里每個(gè)方法在代理類中都要重新實(shí)現(xiàn)一遍。就像上面原始類里有reg、login方法,代理類里也有reg、login方法。
動(dòng)態(tài)代理
如何解決上述的問題,我們可以借助動(dòng)態(tài)代理來解決。想要使用動(dòng)態(tài)代理,就要理解并使用PHP中的反射機(jī)制。
php具有完整的反射 API,添加了對(duì)類、接口、函數(shù)、方法和擴(kuò)展進(jìn)行反向工程的能力。 此外,反射 API 提供了方法來取出函數(shù)、類和方法中的文檔注釋。關(guān)于php的反射相關(guān)知識(shí),這里就不詳述了,大家可以自行查閱相關(guān)信息。
注意,使用反射對(duì)性能消耗很大,一般情況下請(qǐng)不要使用。
下面我們來展示如何用反射實(shí)現(xiàn)動(dòng)態(tài)代理,偽代碼如下:
class UserLogin { public function reg ($uname, $pass) { // 注冊(cè)業(yè)務(wù) echo '注冊(cè)業(yè)務(wù)' . PHP_EOL; } public function login ($uname, $pass) { // 登錄業(yè)務(wù) echo '登錄業(yè)務(wù)' . PHP_EOL; } } class LimitProxy { // 用來保存多個(gè)實(shí)例對(duì)象 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 "這里是限流業(yè)務(wù)處理" . PHP_EOL; $result = $method->isStatic() ? $method->invoke(null, $obj, ...$arguments) : $method->invoke($obj, ...$arguments); return $result; } } } } }
測(cè)試代碼如下:
$login = new Login(); $loginProxy = new LimitProxy($login); $loginProxy->reg('gwx', '111111'); $loginProxy->login('james', '111111111');
應(yīng)用場(chǎng)景
-
訪問控制 (保護(hù)代理)。 比如系統(tǒng)有一個(gè)訂單的模塊,原本該模塊也有權(quán)限控制,但現(xiàn)在我們希望只針對(duì)指定ip的客戶端可以訪問,那么我們可以使用代理模式。
-
本地執(zhí)行遠(yuǎn)程服務(wù) (遠(yuǎn)程代理)適用于服務(wù)對(duì)象位于遠(yuǎn)程服務(wù)器上的情形。
-
在業(yè)務(wù)代碼中開發(fā)一些非功能性的需求,比如:限流、統(tǒng)計(jì)、日志記錄
-
緩存方面的應(yīng)用,比如添加一個(gè)緩存代理,當(dāng)緩存存在時(shí),就調(diào)用緩存代理獲取緩存的數(shù)據(jù),當(dāng)緩存不存在時(shí),就調(diào)用原始接口。