Miles' Blog

天涯何處無幹話,何必要講實務話

Proxy Pattern

代理模式的 代理,其實跟平常大家會提到的 代理伺服器 代理人 等概念,是一模一樣的。

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、會員權限等,與主要商業邏輯無關的物件

利用動態代理的特性,可以一開始專注在主要商業邏輯的實作上,其他功能可以在主功能完成後,再用動態代理的模式無痛追加。

0%