本篇我們將介紹剩餘四種結構型模式,它們分別是:
- 裝飾模式
- 外觀模式
- 享元模式
- 代理模式
裝飾模式
提到裝飾,我們先來想一下生活中有哪些裝飾:
- 女生的首飾:戒指、耳環、項鍊等裝飾品
- 家居裝飾品:粘鉤、鏡子、壁畫、盆栽等等
我們爲什麼需要這些裝飾品呢?我們很容易想到是爲了美,戒指、耳環、項鍊、壁畫、盆栽都是爲了提高顏值或增加美觀度。但粘鉤、鏡子不一樣,它們是爲了方便我們掛東西、洗漱。所以我們可以總結出裝飾品共有兩種功能:
- 增強原有的特性:我們本身就是有一定顏值的,添加裝飾品提高了我們的顏值。同樣地,房屋本身就有一定的美觀度,家居裝飾提高了房屋的美觀度。
- 添加新的特性:在牆上掛上粘鉤,讓牆壁有了掛東西的功能。在洗漱臺裝上鏡子,讓洗漱臺有了照鏡子的功能。
並且我們發現,裝飾品並不會改變物品本身,只是起到一個錦上添花的作用。裝飾模式也一樣,它的主要作用就是:
- 增強一個類原有的功能
- 爲一個類添加新的功能
並且裝飾模式也不會改變原有的類。
裝飾模式:動態地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生成子類實現更爲靈活。其別名也可以稱爲包裝器,與適配器模式的別名相同,但它們適用於不同的場合。根據翻譯的不同,裝飾模式也有人稱之爲“油漆工模式”。
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,到這裏我們就將結構型模式介紹完了,下一篇我們將介紹行爲型模式。