Proxy
December 30, 2021如果你寫了個 HelloSpeaker
,可以指定招呼語,有個 hello
方法可以指定使用者名稱:
class HelloSpeaker {
private String hello;
HelloSpeaker(String hello) {
this.hello = hello;
}
void hello(String name) {
out.printf("%s, %s%n", this.hello, name);
}
}
若有個方法接受 HelloSpeaker
,其中會呼叫 hello
方法:
void doSomething(HelloSpeaker helloSpeaker) {
...
helloSpeaker.hello("Justin");
...
}
現在如果想在 hello
方法執行前後留下日誌(logging),你會怎麼寫呢?修改 HelloSpeaker
?
import java.util.logging.*;
class HelloSpeaker {
private String hello;
HelloSpeaker(String hello) {
this.hello = hello;
}
void hello(String name) {
log("hello 方法開始...."); // 日誌服務
out.printf("%s, %s%n", this.hello, name);
log("hello 方法結束...."); // 日誌服務
}
private void log(String msg) {
Logger.getLogger(HelloProxy.class.getName()).log(Level.INFO, msg);
}
}
還是修改 doSomething
?
void doSomething(HelloSpeaker helloSpeaker) {
...
Logger.getLogger(HelloProxy.class.getName())
.log(Level.INFO, "hello 方法開始....");
helloSpeaker.hello("Justin");
Logger.getLogger(HelloProxy.class.getName())
.log(Level.INFO, "hello 方法結束....");
...
}
若之後不用日誌了呢?改回來嗎?唔…不覺得麻煩嗎?
日誌代理人
有沒有可能將 HelloSpeaker
的日誌抽取出來成為物件,需要日誌的時候,由該物件代為處理,真正的呼叫 hello
時再委話給 HelloSpeaker
實例呢?如果是這樣的話,doSomething
怎麼辦呢?不就需要對日誌代理與 HelloSpeaker
一視同仁嗎?
喔!一視同仁的意思,就表示從 doSomething
角度來看,日誌代理與 HelloSpeaker
要有相同的行為,那就來定義一個 Hello
介面,然後讓日誌代理與 HelloSpeaker
都實作該介面:
interface Hello {
void hello(String name);
}
class HelloSpeaker implements Hello {
private String hello;
HelloSpeaker(String hello) {
this.hello = hello;
}
public void hello(String name) {
out.printf("%s, %s%n", this.hello, name);
}
}
class HelloLoggingProxy implements Hello {
private Hello hello;
HelloLoggingProxy(Hello hello) {
this.hello = hello;
}
public void hello(String name) {
log("hello 方法開始...."); // 日誌服務
out.printf("%s, %s%n", hello.hello, name);
log("hello 方法結束...."); // 日誌服務
}
private void log(String msg) {
Logger.getLogger(HelloProxy.class.getName()).log(Level.INFO, msg);
}
}
doSomething
接受 Hello
實例,
void doSomething(Hello hello) {
...
hello.hello("Justin");
...
}
那麼需要日誌的時候,就 doSomething(new HelloLoggingProxy(new HelloSpeaker("哈囉")))
,不需要日誌的時候就 doSomething(new HelloSpeaker("哈囉"))
,看來方便多了。
在 Gof 模式中,稱這樣的概念為 Proxy 模式,因為代理人與被代理者會具有相同的行為,從客戶端的角度來看,不會知道它在操作的是代理人,還是被代理者。
乍看跟〈Decorator〉有點像,畢竟都是種 wrapper,然而不同點之一方才就講過了,從客戶端來看,代理人與被代理者會具有相同的行為,而 Decorator,作為 decorator 角色的物件,會基於被包裹物件的結果進一步處理,因此會提供的方法可能比被包裹的物件多或者高階,也就是行為不同,另外,代理人做的事,不見得與被代理者處理的事有關,像是日誌,與被代理者就沒有關係。
從原則來看,Proxy 基本上就是里氏替換(Liskov substitution),讓代理人與被代理者可以替換,從客戶端角度來看,替換前有什麼行為,用代理人替換後那些行為不應該改變,若是去除代理人,也只是少了代理人的服務。
Proxy 的實現
只不過,這邊特別定義了一個專用的 Hello
介面,如果其他物件需要日誌服務時,也需要定義專用的介面嗎?這也太麻煩了吧!
定義專用介面的方式,通常被稱為靜態代理,實際上根據你使用的語言或工具而定,Proxy 的實現可以很魔法。
例如,透過 Java 標準 API 的 Proxy.newProxyInstance
等機制,可以動態地建立實現代理時的介面,有興趣可以參考〈動態代理〉。
動態定型語言的話,因為鴨子定型,要實現簡單的靜態代理,不用大費周章,建立一個包裹器,具有相同的方法就可以了:
class HelloSpeaker:
def __init__(self, hello):
self.hello = hello
def hello(self, name):
print(f'{self.hello}, {name}')
class HelloLoggingProxy:
def __init__(self, helloSpeaker):
self.helloSpeaker = helloSpeaker
def hello(self, name):
self.log('hello 方法開始....')
self.helloSpeaker.hello(name)
self.log('hello 方法結束....')
def log(self, msg):
logging.getLogger(__name__).log(logging.INFO, msg)
然而複雜的代理需求,還是得透過 __getattribute__
、__getattr__
、__setattr__
等特殊方法或內省機制之類來實現;類似地,JavaScript 若要實現複雜的代理需求,可以透過 ES6 以後的 Proxy API,來看個簡單的:
let array = [1, 2, 3];
let proxy = new Proxy(array, {
get(target, prop, receiver) {
console.log('get on', target, prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log('set on', target, prop, value);
return Reflect.set(target, prop, value, receiver);
}
});
透過 proxy[0]
、proxy[1] = 10
之類的操作,你會看到主控台有相關對應的訊息,為了將操作委託給被代理者,Proxy API 通常搭配 ES6 以後的 Reflect API,像是上面看到的 Reflect.set
、Reflect.get
,會對目標陣列進行存取。
也就是說,不管你的語言環境,是否有提供 Proxy 字樣的 API,如果你曾經用過反射、自省之類的機制,或者曾經實現過攔截器(interceptor)之類的東西,或許你早就實現過 Proxy 模式了。