設計模式(5)——代理模式
1. 前文彙總
2. 從 LOL 中學習代理模式
我是一個很喜歡玩遊戲的人,雖然平時玩遊戲的時間並不多,但我也是一個忠實的 LOL 的愛好者,就是段位有點慘不忍睹,常年倔強的黑鐵,今年 S10 的總決賽在上海舉行,這個事兒我從 S9 就開始期待,結果門票今年沒賣,直接是抽籤拼人品。
360w+ 人抽 3600+ 人,這個概率屬實有點低,只能找個地方和我的小夥伴一起看了。
打 LOL 最開心的事情莫過於拿到 PentaKill 和 victory ,把這件事情使用程式碼表現出來,首先定義一個玩遊戲的人的介面:
public interface ILOLPlayer {
// 登入使用使用者名稱和密碼
void login(String name, String password);
// 拿到五殺
void pentaKill();
// 遊戲勝利
void victory();
}
第二步對上面的介面做一個實現:
public class LOLPlayer implements ILOLPlayer {
private String name = "";
public LOLPlayer(String name) {
this.name = name;
}
@Override
public void login(String name, String password) {
System.out.println("登入遊戲:name:" + name + ", password:" + password);
}
@Override
public void pentaKill() {
System.out.println(this.name + " 拿到五殺啦!!!");
}
@Override
public void victory() {
System.out.println(this .name + " 遊戲勝利啦!!!");
}
}
最後我們寫一個最簡單的測試類:
public class Test {
public static void main(String[] args) {
LOLPlayer lolPlayer = new LOLPlayer("geekdigging");
lolPlayer.login("geekdigging", "password");
lolPlayer.pentaKill();
lolPlayer.victory();
}
}
執行結果:
登入遊戲:name:geekdigging, password:password
geekdigging 拿到五殺啦!!!
geekdigging 遊戲勝利啦!!!
在打遊戲的過程中,大家都知道有一個型別叫做排位賽,排位賽能到多少段位,一個是看時間,一個是看天賦,基本上打到一定的段位就很難再往上走了,如果說這時候還想升段位,那就只能取找代練幫忙做代打了。
我們找一位代練幫我們繼續打遊戲:
public class LOLPlayerProxy implements ILOLPlayer {
private ILOLPlayer ilolPlayer;
public LOLPlayerProxy(LOLPlayer playerLayer) {
this.ilolPlayer = playerLayer;
}
@Override
public void login(String name, String password) {
this.ilolPlayer.login(name, password);
}
@Override
public void pentaKill() {
this.ilolPlayer.pentaKill();
}
@Override
public void victory() {
this.ilolPlayer.victory();
}
}
我們稍微修改一下測試類:
public class Test {
public static void main(String[] args) {
LOLPlayer lolPlayer = new LOLPlayer("geekdigging");
LOLPlayerProxy proxy = new LOLPlayerProxy(lolPlayer);
proxy.login("geekdigging", "password");
proxy.pentaKill();
proxy.victory();
}
}
這個測試類裡面,我們沒有自己打遊戲,而是使用代練 proxy 來幫我們打遊戲,最後的結果是:
登入遊戲:name:geekdigging, password:password
geekdigging 拿到五殺啦!!!
geekdigging 遊戲勝利啦!!!
這就是代理模式,本來需要自己做事情,使用代理以後,就可以由代理幫我們做事情了。
3. 代理模式定義
代理模式(Proxy Pattern)是一個使用率非常高的模式,其定義如下:
Provide a surrogate or placeholder for another object to control access toit.(為其他物件提供一種代理以控制對這個物件的訪問。)
- Subject: 抽象主題角色。
- RealSubject: 具體主題角色。
- Proxy: 代理主題角色。
通用示例程式碼如下:
// 抽象主題類,定義一個方法
public interface Subject {
void request();
}
// 具體主題類,在這裡寫具體的處理邏輯
public class RealSubject implements Subject {
@Override
public void request() {
// 邏輯處理
}
}
// 代理類
public class Proxy implements Subject {
private Subject subject;
public Proxy() {
this.subject = new Proxy();
}
public Proxy(RealSubject subject) {
this.subject = subject;
}
@Override
public void request() {
this.before();
this.subject.request();
this.after();
}
private void before() {
// 邏輯預處理
}
private void after() {
// 邏輯善後處理
}
}
在最後的這個代理類中,通過建構函式來進行代理角色的傳遞,同時還可以在具體的處理邏輯上構造一個切面,定義預處理邏輯以及善後處理邏輯。
4. 代理模式的優點
- 職責清晰:真實的角色是用來實現具體業務邏輯的,無需關心其他工作,可以後期通過代理的方式來完成其他的工作。
- 高擴充套件性:
- 智慧化:
5. 普通代理
首先說普通代理,它的要求就是客戶端只能訪問代理角色,而不能訪問真實角色,這是比較簡單的。
使用上面最開始的打 LOL 進行改造,我自己作為一個遊戲玩家,我肯定自己不練級了,也就是場景類不能再直接 new 一個 LOLPlayer 物件了,它必須由 LOLPlayerProxy 來進行模擬場景。
首先是對 LOLPlayer 類進行改造,把 LOLPlayer 這個類的構造方法修改,使他不能直接 new 一個物件出來。
public class LOLPlayer implements ILOLPlayer {
private String name;
public LOLPlayer(ILOLPlayer ilolPlayer, String name) throws Exception {
if (ilolPlayer == null) {
throw new Exception("不能建立真實的角色");
} else {
this.name = name;
}
}
// 省略剩餘的程式碼
}
接下來是代理類:
public class LOLPlayerProxy implements ILOLPlayer {
private ILOLPlayer iloLPlayer;
public LOLPlayerProxy(String name) {
try {
iloLPlayer = new LOLPlayer(this, name);
} catch (Exception e) {
e.printStackTrace();
}
}
// 省略剩餘的程式碼
}
代理類也是僅修改了建構函式,通過傳進來的一個代理者的名稱,就能進行代理,在這種改造下,系統更加簡潔了,呼叫者只知道代理存在就可以,不用知道代理了誰。
最後的測試類也需要進行修改:
public class Test {
public static void main(String[] args) {
ILOLPlayer proxy = new LOLPlayerProxy("geekdigging");
proxy.login("geekdigging", "password");
proxy.pentaKill();
proxy.victory();
}
}
在這個代理類上,我沒有再去 new 一個 LOLPlayer 的物件,即可對 LOLPlayer 進行代理。
7. 強制代理
強制代理實際上一個普通代理模式的變種,普通代理是通過代理找到真實的角色,但是強制代理卻是要「強制」,必須通過真實角色查詢到代理角色,否則將不能訪問。
首先是對介面類加一個 getProxy() 方法,指定要訪問自己必須通過哪個代理。
public interface ILOLPlayer {
// 登入使用使用者名稱和密碼
void login(String name, String password);
// 拿到五殺
void pentaKill();
// 遊戲勝利
void victory();
// 獲取自己的代理類
ILOLPlayer getProxy();
}
然後再是對具體實現類的改造:
public class LOLPlayer implements ILOLPlayer {
private String name;
private ILOLPlayer proxy;
public LOLPlayer(String name) {
this.name = name;
}
@Override
public void login(String name, String password) {
if (this.isProxy()) {
System.out.println("登入遊戲:name:" + name + ", password:" + password);
} else {
System.out.println("請使用指定的代理");
}
}
@Override
public void pentaKill() {
if (this.isProxy()) {
System.out.println(this.name + " 拿到五殺啦!!!");
} else {
System.out.println("請使用指定的代理");
}
}
@Override
public void victory() {
if (this.isProxy()) {
System.out.println(this.name + " 遊戲勝利啦!!!");
} else {
System.out.println("請使用指定的代理");
}
}
@Override
public ILOLPlayer getProxy() {
this.proxy = new LOLPlayerProxy(this);
return this.proxy;
}
private boolean isProxy() {
if (this.proxy == null) {
return false;
} else {
return true;
}
}
}
這裡增加了一個私有方法,檢查是否是自己指定的代理,是指定的代理則允許訪問,否則不允許訪問。
接下來是強制代理類的改進:
public class LOLPlayerProxy implements ILOLPlayer {
private ILOLPlayer iloLPlayer;
public LOLPlayerProxy(ILOLPlayer iloLPlayer) {
this.iloLPlayer = iloLPlayer;
}
@Override
public void login(String name, String password) {
this.iloLPlayer.login(name, password);
}
@Override
public void pentaKill() {
this.iloLPlayer.pentaKill();
}
@Override
public void victory() {
this.iloLPlayer.victory();
}
@Override
public ILOLPlayer getProxy() {
return this;
}
}
最後一個是測試類:
public class Test {
public static void main(String[] args) {
test1();
test2();
test3();
}
public static void test1() {
ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
iloLPlayer.login("geekdigging", "password");
iloLPlayer.pentaKill();
iloLPlayer.victory();
}
public static void test2() {
ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
ILOLPlayer proxy = new LOLPlayerProxy(iloLPlayer);
proxy.login("geekdigging", "password");
proxy.pentaKill();
proxy.victory();
}
public static void test3() {
ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
ILOLPlayer proxy = iloLPlayer.getProxy();
proxy.login("geekdigging", "password");
proxy.pentaKill();
proxy.victory();
}
}
這裡我寫了三個測試方法,分別是 test1 、 test2 和 test3 ,執行一下這個測試類,結果如下:
請使用指定的代理
請使用指定的代理
請使用指定的代理
請使用指定的代理
請使用指定的代理
請使用指定的代理
登入遊戲:name:geekdigging, password:password
geekdigging 拿到五殺啦!!!
geekdigging 遊戲勝利啦!!!
可以發現,前兩個方法都沒有正常產生訪問, test1 是直接 new 了一個物件,無法成功訪問,而 test2 雖然是使用了代理,但是結果還是失敗了,因為它指定的並不是真實的物件,這個物件是我們自己手動 new 出來的,當然不行,只有最後一個 test3 是可以正常代理物件的。
強制代理的概念就是要從真實角色查詢到代理角色,不允許直接訪問真實角色。高層模組只要呼叫 getProxy 就可以訪問真實角色的所有方法,它根本就不需要產生一個代理出來,代理的管理已經由真實角色自己完成。
6. 動態代理
動態代理是在實現階段不用關心代理誰,而在執行階段才指定代理哪一個物件。相對來說,自己寫代理類的方式就是靜態代理。
實現動態代理,主要有兩種方式,一種是通過 JDK 為我們提供的 InvocationHandler 介面,另一種是使用 cglib 。
把上面的案例接著改成動態代理的方式:
增加一個 LOLPlayIH 動態代理類,來實現 InvocationHandler 介面。
public class LOLPlayIH implements InvocationHandler {
Object object;
public LOLPlayIH(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(this.object, args);
return result;
}
}
這裡的 invoke 方法是介面 InvocationHandler 定義必須實現的,它完成對真實方法的呼叫。
接下來是測試類:
public class Test {
public static void main(String[] args) {
ILOLPlayer ilolPlayer = new LOLPlayer("geekdigging");
InvocationHandler handler = new LOLPlayIH(ilolPlayer);
ClassLoader loader = ilolPlayer.getClass().getClassLoader();
ILOLPlayer proxy = (ILOLPlayer) Proxy.newProxyInstance(loader, new Class[] {ILOLPlayer.class}, handler);
proxy.login("geekdigging", "password");
proxy.pentaKill();
proxy.victory();
}
}
這裡我們沒有建立代理類,也沒有實現 ILOLPlayer 介面,但我們還是讓代練在幫我們上分,這就是動態代理。
接下來看下 CGLIB 代理的方式,修改前面的代理類:
public class CglibProxy implements MethodInterceptor {
private Object target;
public Object getInstance(final Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object result = methodProxy.invoke(this.target, objects);
return result;
}
}
編寫新的測試類:
public class Test {
public static void main(String[] args) {
ILOLPlayer ilolPlayer = new LOLPlayer("geekdigging");
CglibProxy proxy = new CglibProxy();
LOLPlayer lolPlayer = (LOLPlayer) proxy.getInstance(ilolPlayer);
lolPlayer.login("geekdigging", "password");
lolPlayer.pentaKill();
lolPlayer.victory();
}
}
這裡有一點需要注意, CGLIB 動態代理需要具體物件擁有無參構造,需要我們手動在 LOLPlayer 中新增一個無參建構函式。