設計模式(三) —— 結構型模式(下)

本篇我們將介紹剩餘四種結構型模式,它們分別是:

  • 裝飾模式
  • 外觀模式
  • 享元模式
  • 代理模式

裝飾模式

提到裝飾,我們先來想一下生活中有哪些裝飾:

  • 女生的首飾:戒指、耳環、項鍊等裝飾品
  • 家居裝飾品:粘鉤、鏡子、壁畫、盆栽等等

我們爲什麼需要這些裝飾品呢?我們很容易想到是爲了美,戒指、耳環、項鍊、壁畫、盆栽都是爲了提高顏值或增加美觀度。但粘鉤、鏡子不一樣,它們是爲了方便我們掛東西、洗漱。所以我們可以總結出裝飾品共有兩種功能:

  • 增強原有的特性:我們本身就是有一定顏值的,添加裝飾品提高了我們的顏值。同樣地,房屋本身就有一定的美觀度,家居裝飾提高了房屋的美觀度。
  • 添加新的特性:在牆上掛上粘鉤,讓牆壁有了掛東西的功能。在洗漱臺裝上鏡子,讓洗漱臺有了照鏡子的功能。

並且我們發現,裝飾品並不會改變物品本身,只是起到一個錦上添花的作用。裝飾模式也一樣,它的主要作用就是:

  • 增強一個類原有的功能
  • 爲一個類添加新的功能

並且裝飾模式也不會改變原有的類。

裝飾模式:動態地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生成子類實現更爲靈活。其別名也可以稱爲包裝器,與適配器模式的別名相同,但它們適用於不同的場合。根據翻譯的不同,裝飾模式也有人稱之爲“油漆工模式”。

1. 用於增強功能的裝飾模式

我們用程序來模擬一下戴上裝飾品提高我們顏值的過程:

新建顏值接口:

public interface IBeauty {
    int getBeautyValue();
}

新建 Me 類,實現顏值接口:

public class Me implements IBeauty {

    @Override
    public int getBeautyValue() {
        return 100;
    }
}

戒指裝飾類,將 Me 包裝起來:

public class RingDecorator implements IBeauty {
    private final IBeauty me;

    public RingDecorator(IBeauty me) {
        this.me = me;
    }

    @Override
    public int getBeautyValue() {
        return me.getBeautyValue() + 20;
    }
}

客戶端測試:

public class Client {
    @Test
    public void show() {
        IBeauty me = new Me();
        System.out.println("我原本的顏值:" + me.getBeautyValue());

        IBeauty meWithRing = new RingDecorator(me);
        System.out.println("戴上了戒指後,我的顏值:" + meWithRing.getBeautyValue());
    }
}

運行程序,輸出如下:

我原本的顏值:100
戴上了戒指後,我的顏值:120

這就是最簡單的增強功能的裝飾模式。以後我們可以添加更多的裝飾類,比如:

耳環裝飾類:

public class EarringDecorator implements IBeauty {
    private final IBeauty me;

    public EarringDecorator(IBeauty me) {
        this.me = me;
    }

    @Override
    public int getBeautyValue() {
        return me.getBeautyValue() + 50;
    }
}

項鍊裝飾類:

public class NecklaceDecorator implements IBeauty {
    private final IBeauty me;

    public NecklaceDecorator(IBeauty me) {
        this.me = me;
    }

    @Override
    public int getBeautyValue() {
        return me.getBeautyValue() + 80;
    }
}

客戶端測試:

public class Client {
    @Test
    public void show() {
        IBeauty me = new Me();
        System.out.println("我原本的顏值:" + me.getBeautyValue());

        // 隨意挑選裝飾
        IBeauty meWithNecklace = new NecklaceDecorator(me);
        System.out.println("戴上了項鍊後,我的顏值:" + meWithNecklace.getBeautyValue());

        // 多次裝飾
        IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));
        System.out.println("戴上耳環、戒指、項鍊後,我的顏值:" + meWithManyDecorators.getBeautyValue());

        // 任意搭配裝飾
        IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new RingDecorator(me));
        System.out.println("戴上戒指、項鍊後,我的顏值:" + meWithNecklaceAndRing.getBeautyValue());
    }
}

運行程序,輸出如下:

我原本的顏值:100
戴上了項鍊後,我的顏值:180
戴上耳環、戒指、項鍊後,我的顏值:250
戴上戒指、項鍊後,我的顏值:200

可以看到,裝飾器也實現了 IBeauty 接口,並且沒有添加新的方法,也就是說這裏的裝飾器僅用於增強功能,並不會改變 Me 原有的功能,這種裝飾模式稱之爲透明裝飾模式,由於沒有改變接口,也沒有新增方法,所以透明裝飾模式可以無限裝飾

裝飾模式是繼承的一種替代方案。本例如果不使用裝飾模式,而是改用繼承實現的話,戴着戒指的 Me 需要派生一個子類、戴着項鍊的 Me 需要派生一個子類、戴着耳環的 Me 需要派生一個子類、戴着戒指 + 項鍊的需要派生一個子類…各種各樣的排列組合會造成類爆炸。而採用了裝飾模式就只需要爲每個裝飾品生成一個裝飾類即可,所以說就增加對象功能來說,裝飾模式比生成子類實現更爲靈活

2. 用於添加功能的裝飾模式

我們用程序來模擬一下房屋裝飾粘鉤後,新增了掛東西功能的過程:

新建房屋接口:

public interface IHouse {
    void live();
}

房屋類:

public class House implements IHouse{

    @Override
    public void live() {
        System.out.println("房屋原有的功能:居住功能");
    }
}

新建粘鉤裝飾器接口,繼承自房屋接口:

public interface IStickyHookHouse extends IHouse{
    void hangThings();
}

粘鉤裝飾類:

public class StickyHookDecorator implements IStickyHookHouse {
    private final IHouse house;

    public StickyHookDecorator(IHouse house) {
        this.house = house;
    }

    @Override
    public void live() {
        house.live();
    }

    @Override
    public void hangThings() {
        System.out.println("有了粘鉤後,新增了掛東西功能");
    }
}

客戶端測試:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();

        IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
        stickyHookHouse.live();
        stickyHookHouse.hangThings();
    }
}

運行程序,顯示如下:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘鉤後,新增了掛東西功能

這就是用於新增功能的裝飾模式。我們在接口中新增了方法:hangThings,然後在裝飾器中將 House 類包裝起來,之前 House 中的方法仍然調用 house 去執行,也就是說我們並沒有修改原有的功能,只是擴展了新的功能,這種模式在裝飾模式中稱之爲半透明裝飾模式。

爲什麼叫半透明呢?由於新的接口 IStickyHookHouse 擁有之前 IHouse 不具有的方法,所以我們如果要使用裝飾器中添加的功能,就不得不區別對待裝飾前的對象和裝飾後的對象。也就是說客戶端要使用新方法,必須知道具體的裝飾類 StickyHookDecorator,所以這個裝飾類對客戶端來說是可見的、不透明的。而被裝飾者不一定要是 House,它可以是實現了 IHouse 接口的任意對象,所以被裝飾者對客戶端是不可見的、透明的。由於一半透明,一半不透明,所以稱之爲半透明裝飾模式。

我們可以添加更多的裝飾器:

新建鏡子裝飾器的接口,繼承自房屋接口:

public interface IMirrorHouse extends IHouse {
    void lookMirror();
}

鏡子裝飾類:

public class MirrorDecorator implements IMirrorHouse{
    private final IHouse house;

    public MirrorDecorator(IHouse house) {
        this.house = house;
    }

    @Override
    public void live() {
        house.live();
    }

    @Override
    public void lookMirror() {
        System.out.println("有了鏡子後,新增了照鏡子功能");
    }
}

客戶端測試:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();

        IMirrorHouse mirrorHouse = new MirrorDecorator(house);
        mirrorHouse.live();
        mirrorHouse.lookMirror();
    }
}

運行程序,輸出如下:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了鏡子後,新增了照鏡子功能

現在我們仿照透明裝飾模式的寫法,同時添加粘鉤和鏡子裝飾試一試:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();

        IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
        IMirrorHouse houseWithStickyHookMirror = new MirrorDecorator(stickyHookHouse);
        houseWithStickyHookMirror.live();
        houseWithStickyHookMirror.hangThings(); // 這裏會報錯,找不到 hangThings 方法
        houseWithStickyHookMirror.lookMirror();
    }
}

我們會發現,第二次裝飾時,無法獲得上一次裝飾添加的方法。原因很明顯,當我們用 IMirrorHouse 裝飾器後,接口變爲了 IMirrorHouse,這個接口中並沒有 hangThings 方法。

那麼我們能否讓 IMirrorHouse 繼承自 IStickyHookHouse,以實現新增兩個功能呢?可以,但那樣做的話兩個裝飾類之間有了依賴關係,那就不是裝飾模式了。裝飾類不應該存在依賴關係,而應該在原本的類上進行裝飾。這就意味着,半透明裝飾模式中,我們無法多次裝飾

有的同學會問了,既增強了功能,又添加了新功能的裝飾模式叫什麼呢?

—— 舉一反三,肯定是叫全不透明裝飾模式!

—— 並不是!只要添加了新功能的裝飾模式都稱之爲半透明裝飾模式,他們都具有不可以多次裝飾的特點。仔細理解上文半透明名稱的由來就知道了,“透明”指的是我們無需知道被裝飾者具體的類,既增強了功能,又添加了新功能的裝飾模式仍然具有半透明特性。

看了這兩個簡單的例子,是不是發現裝飾模式很簡單呢?恭喜你學會了 1 + 1 = 2,現在你已經掌握了算數的基本思想,接下來我們來做一道微積分題練習一下。

3.I/O 中的裝飾模式

I/O 指的是 Input/Output,即輸入、輸出。我們以 Input 爲例。先在 src 文件夾下新建一個文件 readme.text,隨便寫點文字:

禁止套娃
禁止禁止套娃
禁止禁止禁止套娃

然後用 Java 的 InputStream 讀取,代碼一般長這樣:

public void io() throws IOException {
    InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
    byte[] buffer = new byte[1024];
    while (in.read(buffer) != -1) {
        System.out.println(new String(buffer));
    }
    in.close();
}

這樣寫有一個問題,如果讀取過程中出現了 IO 異常,InputStream 就不能正確的關閉,所以我們要用 try...finally來保證 InputStream 正確關閉:

public void io() throws IOException {
    InputStream in = null;
    try {
        in = new BufferedInputStream(new FileInputStream("src/readme.txt"));
        byte[] buffer = new byte[1024];
        while (in.read(buffer) != -1) {
            System.out.println(new String(buffer));
        }
    } finally {
        if (in != null) {
            in.close();
        }
    }
}

這種寫法實在是太醜了,而 IO 操作又必須這麼寫,顯然 Java 也意識到了這個問題,所以 Java 7 中引入了 try(resource)語法糖,IO 的代碼就可以簡化如下:

public void io() throws IOException {
    try (InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"))) {
        byte[] buffer = new byte[1024];
        while (in.read(buffer) != -1) {
            System.out.println(new String(buffer));
        }
    }
}

這種寫法和上一種邏輯是一樣的,運行程序,顯示如下:

禁止套娃
禁止禁止套娃
禁止禁止禁止套娃

觀察獲取 InputStream 這句代碼:

InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt"));

是不是和我們之前多次裝飾的代碼非常相似:

// 多次裝飾
IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));

事實上,查看 I/O 的源碼可知,Java I/O 的設計框架便是使用的裝飾者模式,InputStream 的繼承關係如下:

其中,InputStream 是一個抽象類,對應上文例子中的 IHouse,其中最重要的方法是 read 方法,這是一個抽象方法:

public abstract class InputStream implements Closeable {
    
    public abstract int read() throws IOException;
    
    // ...
}

這個方法會讀取輸入流的下一個字節,並返回字節表示的 int 值(0~255),返回 -1 表示已讀到末尾。由於它是抽象方法,所以具體的邏輯交由子類實現。

上圖中,左邊的三個類 FileInputStream、ByteArrayInputStream、ServletInputStream 是 InputStream 的三個子類,對應上文例子中實現了 IHouse 接口的 House。

右下角的三個類 BufferedInputStream、DataInputStream、CheckedInputStream 是三個具體的裝飾者類,他們都爲 InputStream 增強了原有功能或添加了新功能。

FilterInputStream 是所有裝飾類的父類,它沒有實現具體的功能,僅用來包裝了一下 InputStream:

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }
    
    //...
}

我們以 BufferedInputStream 爲例。原有的 InputStream 讀取文件時,是一個字節一個字節的讀取的,這種方式的執行效率並不高,所以我們可以設立一個緩衝區,先將內容讀取到緩衝區中,緩衝區讀滿後,將內容從緩衝區中取出來,這樣就變成了一段一段的讀取,用內存換取效率。BufferedInputStream 就是用來做這個的。它繼承自 FilterInputStream:

public class BufferedInputStream extends FilterInputStream {
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    protected volatile byte buf[];

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    
    //...
}

我們先來看它的構造方法,在構造方法中,新建了一個 byte[] 作爲緩衝區,從源碼中我們看到,Java 默認設置的緩衝區大小爲 8192 byte,也就是 8 KB。

然後我們來查看 read 方法:

public class BufferedInputStream extends FilterInputStream {
    //...

    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }

    private void fill() throws IOException {
        // 往緩衝區內填充讀取內容的過程
        //...
    }
}

在 read 方法中,調用了 fill 方法,fill 方法的作用就是往緩衝區中填充讀取的內容。這樣就實現了增強原有的功能。

在源碼中我們發現,BufferedInputStream 沒有添加 InputStream 中沒有的方法,所以 BufferedInputStream 使用的是透明的裝飾模式。

DataInputStream 用於更加方便的讀取 int、double 等內容,觀察 DataInputStream 的源碼可以發現,DataInputStream 中新增了 readInt、readLong 等方法,所以 DataInputStream 使用的是半透明裝飾模式。

理解了 InputStream 後,再看一下 OutputStream 的繼承關係,相信大家一眼就能看出各個類的作用了:

這就是裝飾模式,注意不要和適配器模式混淆了。兩者在使用時都是包裝一個類,但兩者的區別其實也很明顯:

  • 純粹的適配器模式僅用於改變接口,不改變其功能,部分情況下我們需要改變一點功能以適配新接口。但使用適配器模式時,接口一定會有一個回爐重造的過程。
  • 裝飾模式不改變原有的接口,僅用於增強原有功能或添加新功能,強調的是錦上添花

掌握了裝飾者模式之後,理解 Java I/O 的框架設計就非常容易了。但對於不理解裝飾模式的人來說,各種各樣相似的 InputStream 非常容易讓開發者感到困惑。這一點正是裝飾模式的缺點:容易造成程序中有大量相似的類。雖然這更像是開發者的缺點,我們應該做的是提高自己的技術,掌握了這個設計模式之後它就是我們的一把利器。現在我們再看到 I/O 不同的 InputStream 裝飾類,只需要關注它增強了什麼功能或添加了什麼功能即可。

外觀模式

外觀模式非常簡單,體現的就是 Java 中封裝的思想。將多個子系統封裝起來,提供一個更簡潔的接口供外部調用。

外觀模式:外部與一個子系統的通信必須通過一個統一的外觀對象進行,爲子系統中的一組接口提供一個一致的界面,外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。外觀模式又稱爲門面模式。

舉個例子,比如我們每天打開電腦時,都需要做三件事:

  • 打開瀏覽器
  • 打開 IDE
  • 打開微信

每天下班時,關機前需要做三件事:

  • 關閉瀏覽器
  • 關閉 IDE
  • 關閉微信

用程序模擬如下:

新建瀏覽器類:

public class Browser {
    public static void open() {
        System.out.println("打開瀏覽器");
    }

    public static void close() {
        System.out.println("關閉瀏覽器");
    }
}

新建 IDE 類:

public class IDE {
    public static void open() {
        System.out.println("打開 IDE");
    }

    public static void close() {
        System.out.println("關閉 IDE");
    }
}

新建微信類:

public class Wechat {
    public static void open() {
        System.out.println("打開微信");
    }

    public static void close() {
        System.out.println("關閉微信");
    }
}

客戶端調用:

public class Client {
    @Test
    public void test() {
        System.out.println("上班:");
        Browser.open();
        IDE.open();
        Wechat.open();

        System.out.println("下班:");
        Browser.close();
        IDE.close();
        Wechat.close();
    }
}

運行程序,輸出如下:

上班:
打開瀏覽器
打開 IDE
打開微信
下班:
關閉瀏覽器
關閉 IDE
關閉微信

由於我們每天都要做這幾件事,所以我們可以使用外觀模式,將這幾個子系統封裝起來,提供更簡潔的接口:

public class Facade {
    public void open() {
        Browser.open();
        IDE.open();
        Wechat.open();
    }

    public void close() {
        Browser.close();
        IDE.close();
        Wechat.close();
    }
}

客戶端就可以簡化代碼,只和這個外觀類打交道:

public class Client {
    @Test
    public void test() {
        Facade facade = new Facade();
        System.out.println("上班:");
        facade.open();

        System.out.println("下班:");
        facade.close();
    }
}

運行程序,輸出與之前一樣。

外觀模式就是這麼簡單,它使得兩種不同的類不用直接交互,而是通過一箇中間件——也就是外觀類——間接交互。外觀類中只需要暴露簡潔的接口,隱藏內部的細節,所以說白了就是封裝的思想。

外觀模式非常常用,(當然了!寫代碼哪有不封裝的!)尤其是在第三方庫的設計中,我們應該提供儘量簡潔的接口供別人調用。另外,在 MVC 架構中,C 層(Controller)就可以看作是外觀類,Model 和 View 層通過 Controller 交互,減少了耦合。

享元模式

享元模式體現的是程序可複用的特點,爲了節約寶貴的內存,程序應該儘可能地複用,就像《極限編程》作者 Kent 在書裏說到的那樣:Don’t repeat yourself. 簡單來說享元模式就是共享對象,提高複用性,官方的定義倒是顯得文縐縐的:

享元模式:運用共享技術有效地支持大量細粒度對象的複用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次複用。由於享元模式要求能夠共享的對象必須是細粒度對象,因此它又稱爲輕量級模式。

有個細節值得注意:有些對象本身不一樣,但通過一點點變化後就可以複用,我們編程時可能稍不注意就會忘記複用這些對象。比如說偉大的超級瑪麗,誰能想到草和雲更改一下顏色就可以實現複用呢?

還有裏面的三種烏龜,換一個顏色、加一個裝飾就變成了不同的怪:

在超級瑪麗中,這樣的細節還有很多,正是這些精湛的複用使得這一款紅遍全球的遊戲僅有 40KB 大小。正是印證了那句名言:神在細節之中。

代理模式

現在我們有一個類,他整天就只負責吃飯、睡覺:

類的接口

public interface IPerson {
    void eat();
    void sleep();
}

類:

public class Person implements IPerson{

    @Override
    public void eat() {
        System.out.println("我在吃飯");
    }

    @Override
    public void sleep() {
        System.out.println("我在睡覺");
    }
}

客戶端測試:

public class Client {
    @Test
    public void test() {
        Person person = new Person();
        person.eat();
        person.sleep();
    }
}

運行程序,輸出如下:

我在吃飯
我在睡覺

我們可以把這個類包裝到另一個類中,實現完全一樣的行爲:

public class PersonProxy implements IPerson {

    private final Person person;

    public PersonProxy(Person person) {
        this.person = person;
    }

    @Override
    public void eat() {
        person.eat();
    }

    @Override
    public void sleep() {
        person.sleep();
    }
}

將客戶端修改爲調用這個新的類:

public class Client {
    @Test
    public void test() {
        Person person = new Person();
        PersonProxy proxy = new PersonProxy(person);
        proxy.eat();
        proxy.sleep();
    }
}

運行程序,輸出如下:

我在吃飯
我在睡覺

這就是代理模式。

筆者力圖用最簡潔的代碼講解此模式,只要理解了上述這個簡單的例子,你就知道代理模式是怎麼一回事了。我們在客戶端和 Person 類之間新增了一箇中間件 PersonProxy,這個類就叫做代理類,他實現了和 Person 類一模一樣的行爲。

代理模式:給某一個對象提供一個代理,並由代理對象控制對原對象的引用。

現在這個代理類還看不出任何意義,我們來模擬一下工作中的需求。在實際工作中,我們可能會遇到這樣的需求:在網絡請求前後,分別打印將要發送的數據和接收到數據作爲日誌信息。此時我們就可以新建一個網絡請求的代理類,讓它代爲處理網絡請求,並在代理類中打印這些日誌信息。

新建網絡請求接口:

public interface IHttp {
    void request(String sendData);

    void onSuccess(String receivedData);
}

新建 Http 請求工具類:

public class HttpUtil implements IHttp {
    @Override
    public void request(String sendData) {
        System.out.println("網絡請求中...");
    }

    @Override
    public void onSuccess(String receivedData) {
        System.out.println("網絡請求完成。");
    }
}

新建 Http 代理類:

public class HttpProxy implements IHttp {
    private final HttpUtil httpUtil;

    public HttpProxy(HttpUtil httpUtil) {
        this.httpUtil = httpUtil;
    }

    @Override
    public void request(String sendData) {
        httpUtil.request(sendData);
    }

    @Override
    public void onSuccess(String receivedData) {
        httpUtil.onSuccess(receivedData);
    }
}

到這裏,和我們上述吃飯睡覺的代碼是一模一樣的,現在我們在 HttpProxy 中新增打印日誌信息:

public class HttpProxy implements IHttp {
    private final HttpUtil httpUtil;

    public HttpProxy(HttpUtil httpUtil) {
        this.httpUtil = httpUtil;
    }

    @Override
    public void request(String sendData) {
        System.out.println("發送數據:" + sendData);
        httpUtil.request(sendData);
    }

    @Override
    public void onSuccess(String receivedData) {
        System.out.println("收到數據:" + receivedData);
        httpUtil.onSuccess(receivedData);
    }
}

客戶端驗證:

public class Client {
    @Test
    public void test() {
        HttpUtil httpUtil = new HttpUtil();
        HttpProxy proxy = new HttpProxy(httpUtil);
        proxy.request("request data");
        proxy.onSuccess("received result");
    }
}

運行程序,輸出如下:

發送數據:request data
網絡請求中...
收到數據:received result
網絡請求完成。

這就是代理模式的一個應用,除了打印日誌,它還可以用來做權限管理。讀者看到這裏可能已經發現了,這個代理類看起來和裝飾模式的 FilterInputStream 一模一樣,但兩者的目的不同,裝飾模式是爲了增強功能或添加功能,代理模式主要是爲了加以控制

動態代理

上例中的代理被稱之爲靜態代理,動態代理與靜態代理的原理一模一樣,只是換了一種寫法。使用動態代理,需要把一個類傳入,然後根據它正在調用的方法名判斷是否需要加以控制。用僞代碼表示如下:

public class HttpProxy {
    private final HttpUtil httpUtil;

    public HttpProxy(HttpUtil httpUtil) {
        this.httpUtil = httpUtil;
    }

    // 假設調用 httpUtil 的任意方法時,都要通過這個方法間接調用, methodName 表示方法名,args 表示方法中傳入的參數
    public visit(String methodName, Object[] args) {
        if (methodName.equals("request")) {
            // 如果方法名是 request,打印日誌,並調用 request 方法,args 的第一個值就是傳入的參數
            System.out.println("發送數據:" + args[0]);
            httpUtil.request(args[0].toString());
        } else if (methodName.equals("onSuccess")) {
            // 如果方法名是 onSuccess,打印日誌,並調用 onSuccess 方法,args 的第一個值就是傳入的參數
            System.out.println("收到數據:" + args[0]);
            httpUtil.onSuccess(args[0].toString());
        }
    }
}

僞代碼看起來還是很簡單的,實現起來唯一的難點就是怎麼讓 httpUtil 調用任意方法時,都通過一個方法間接調用。這裏需要用到反射技術,不瞭解反射技術也沒有關係,不妨把它記做固定的寫法。實際的動態代理類代碼如下:

public class HttpProxy implements InvocationHandler {
    private HttpUtil httpUtil;

    public IHttp getInstance(HttpUtil httpUtil) {
        this.httpUtil = httpUtil;
        return (IHttp) Proxy.newProxyInstance(httpUtil.getClass().getClassLoader(), httpUtil.getClass().getInterfaces(), this);
    }

    // 調用 httpUtil 的任意方法時,都要通過這個方法調用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        if (method.getName().equals("request")) {
            // 如果方法名是 request,打印日誌,並調用 request 方法
            System.out.println("發送數據:" + args[0]);
            result = method.invoke(httpUtil, args);
        } else if (method.getName().equals("onSuccess")) {
            // 如果方法名是 onSuccess,打印日誌,並調用 onSuccess 方法
            System.out.println("收到數據:" + args[0]);
            result = method.invoke(httpUtil, args);
        }
        return result;
    }
}

先看 getInstance 方法,Proxy.newProxyInstance 方法是 Java 系統提供的方法,專門用於動態代理。其中傳入的第一個參數是被代理的類的 ClassLoader,第二個參數是被代理類的 Interfaces,這兩個參數都是 Object 中的,每個類都有,這裏就是固定寫法。我們只要知道系統需要這兩個參數才能讓我們實現我們的目的:調用被代理類的任意方法時,都通過一個方法間接調用。現在我們給系統提供了這兩個參數,系統就會在第三個參數中幫我們實現這個目的。

第三個參數是 InvocationHandler 接口,這個接口中只有一個方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

那麼不用猜就知道,現在我們調用被代理類 httpUtil 的任意方法時,都會通過這個 invoke 方法調用了。invoke 方法中,第一個參數我們暫時用不上,第二個參數 method 就是調用的方法,使用 method.getName() 可以獲取到方法名,第三個參數是調用 method 方法需要傳入的參數。本例中無論 request 還是 onSuccess 都只有一個 String 類型的參數,對應到這裏就是 args[0]。返回的 Object 是 method 方法的返回值,本例中都是無返回值的。

我們在 invoke 方法中判斷了當前調用方法的方法名,如果現在調用的方法是 request,那麼打印請求參數,並使用這一行代碼繼續執行當前方法:

result = method.invoke(httpUtil, args);

這就是反射調用函數的寫法,如果不瞭解可以記做固定寫法,想要了解的同學可以看之前的這篇文章:詳解Java 反射。雖然這個函數沒有返回值,但我們還是將 result 返回,這是標準做法。

如果現在調用的方法是 onSuccess,那麼打印接收到的數據,並反射繼續執行當前方法。

修改客戶端驗證一下:

public class Client {
    @Test
    public void test() {
        HttpUtil httpUtil = new HttpUtil();
        IHttp proxy = new HttpProxy().getInstance(httpUtil);
        proxy.request("request data");
        proxy.onSuccess("received result");
    }
}

運行程序,輸出與之前一樣:

發送數據:request data
網絡請求中...
收到數據:received result
網絡請求完成。

動態代理本質上與靜態代理沒有區別,它的好處是節省代碼量。比如被代理類有 20 個方法,而我們只需要控制其中的兩個方法,就可以用動態代理通過方法名對被代理類進行動態的控制,而如果用靜態方法,我們就需要將另外的 18 個方法也寫出來,非常繁瑣。這就是動態代理的優勢所在。

OK,到這裏我們就將結構型模式介紹完了,下一篇我們將介紹行爲型模式。

發佈了61 篇原創文章 · 獲贊 56 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章