代理模式的 代理,其實跟平常大家會提到的 代理伺服器 代理人 等概念,是一模一樣的。
Story
有一份工作叫佈署,佈署需要文件才能執行
interface Worker { public function deploy($document); }
|
今天公司只有一個工程師 Alex 會做佈署。
class Alex implements Worker { public function deploy($document) { echo "文件是這樣的,", $document, PHP_EOL; echo "已執行完畢", PHP_EOL; } }
|
但每次佈署文件丟過來,都是東缺西缺的,所以 Alex 決定找個代理人 Miles,來幫他檢查文件。
即然是代理人了,被授權做一樣的工作是很合理的
class Miles implements Worker { private $worker;
public function __construct(Worker $worker) { $this->worker = $worker; }
public function deploy($document) { $this->checkDocument($document); $this->worker->deploy($document); }
public function checkDocument($document) { echo "文件已檢查", PHP_EOL; return true; } }
|
再來大家要佈署程式,可以自己檢查過再給 Alex,或是直接找 Miles 處理了。
class Client { public function main() { $document = "上去 git pull 就對了。";
echo "直接請 Alex 佈署", PHP_EOL; echo "-------------------", PHP_EOL;
$alex = new Alex(); $alex->deploy($document);
echo "-------------------", PHP_EOL;
echo "找代理人 Miles 佈署", PHP_EOL; echo "-------------------", PHP_EOL;
$miles = new Miles($alex); $miles->deploy($document); } }
|
找代理處理事情,一樣可以執行的很好,而且文件也是檢查完成才佈署
Definition
Proxy Pattern 的定義:
Provide a surrogate or placeholder for another object to control access to it.
為物件提供代理,以便在代理物件裡控制存取原物件。
Context
- 已有一個功能已經實作完成的物件
- 有需要在這個功能上加上非功能性的需求,比方說控制物件產生、初始化、存取控制,因為直接修改原有物件會違反基本的物件導向設計原則
- Client 不需要知道真實物件內部實作。包括它怎麼產生、怎麼執行的
UML
通用類別圖如下:
Plantuml 程式碼如下:
@startuml interface Subject { + {abstract} Request() } class RealSubject class Proxy
|
Subject
各角色定義如下:
- Subject 抽象主題,一個介面或抽象類別
- RealSubject 真實角色,真實實作業務的角色
- Proxy 代理角色,它不負責實作業務,只負責應用真實角色
Pros and Cons
Pros
- 代理角色與真實角色的職責很清楚,符合 Single Responsibility Principle 原則
- 使用代理角色協調 Clinet 與真實角色,可適度降低系統耦合性
- 使用代理角色限制 Client 對真實角色的操作權限
Cons
- 客戶端到真實角色間多了代理角色協調,效能會降低。當代理角色處理的事物繁多時,會更明顯。
代理模式有兩種模式:
普通代理
上述情境即是最基本的 普通代理,任何人都能當 Alex 代理,只要能給 Alex 他要的文件即可。
強制代理
現實有種情況是,PM 不想透過代理人,想直接找 Alex 佈署,但 Alex 說:「我很忙,麻煩找我的代理人處理」。這種情境稱之為 強制代理 --真實角色只承認它指定的代理。實作如下:
class Alex implements Worker { private $proxy;
public function getProxy() { $this->proxy = new Miles($this); return $this->proxy; }
public function deploy($document) { if ($this->isProxy()) { echo "文件是這樣的,", $document, PHP_EOL; echo "已執行完畢", PHP_EOL; } else { echo "麻煩找我的代理人處理", PHP_EOL; } }
public function isProxy() { return $this->proxy !== null; } }
|
以後要佈署前都要問 Alex 要找哪個代理人才行了
class Client { public function main() { $document = "上去 git pull 就對了。";
echo "直接請 Alex 佈署", PHP_EOL; echo "-------------------", PHP_EOL;
$alex = new Alex(); $alex->deploy($document);
echo "-------------------", PHP_EOL;
echo "問 Alex 的代理是誰", PHP_EOL; echo "-------------------", PHP_EOL;
$proxy = $alex->getProxy(); $proxy->deploy($document); } }
|
Extensions
代理模式在擴展功能是很方便的,主要有兩種方法。其中之一在故事的例子已經出現:在實作代理物件的同時,加入額外的行為 (檢查文件)
另一種稱之為 動態代理,動態代理的特色是,代理物件的功能是動態產生的。
因為是動態的,所以需要利用很多耗效能的反射機制,使用上需多加考量
首先,先寫一個動態代理物件:
class Delegator { private $target;
public function __construct($target) { $this->target = new $target(); }
public function __call($name, $args) { $r = new ReflectionClass($this->target); if ($method = $r->getMethod($name)) { if ($method->isPublic() && !$method->isAbstract()) { return $method->invokeArgs($this->target, $args); } } } }
|
這個物件沒有實作任何東西,看起來不知道能幹嘛。換來看 Client 怎麼使用:
class Client { public function main() { $document = "上去 git pull 就對了。";
echo "產生動態代理物件", PHP_EOL;
$proxy = new Delegator('Alex');
echo "執行佈署", PHP_EOL;
echo "-------------------", PHP_EOL;
$proxy->deploy($document); } }
|
Delegator 沒實作,場景沒建構 Alex 物件,可是很神奇的程式卻能執行成功?這就是動態代理!
如果想在佈署前檢查文件的話,只要改一下動態代理:
class Delegator { private $target;
public function __construct($target) { $this->target = new $target(); }
public function __call($name, $args) { if ($name == 'deploy') { echo "文件已檢查", PHP_EOL; }
$r = new ReflectionClass($this->target); if ($method = $r->getMethod($name)) { if ($method->isPublic() && !$method->isAbstract()) { return $method->invokeArgs($this->target, $args); } } } }
|
上述程式是硬刻在 Delegator 上。實作上會 extract class 並都可以事後在動態代理上調整追加,最終會變成主要物件和其他物件,而整合是由 Client 整合。
以一個購物車的物件為例,可能會有以下物件:
- 客戶購買商品主要商業邏輯的物件
- 代理物件只做呼叫或轉發訊息
- 其他功能如: Log、會員權限等,與主要商業邏輯無關的物件
利用動態代理的特性,可以一開始專注在主要商業邏輯的實作上,其他功能可以在主功能完成後,再用動態代理的模式無痛追加。