「補課」進行時:設計模式(5)——從 LOL 中學習代理模式

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. 代理模式的優點

  1. 職責清晰:真實的角色是用來實現具體業務邏輯的,無需關心其他工作,可以後期通過代理的方式來完成其他的工作。
  2. 高擴展性:
  3. 智能化:

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 中添加一個無參構造函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章