設計模式之美 - 50 | 裝飾器模式:通過剖析Java IO類庫源碼學習裝飾器模式

這系列相關博客,參考 設計模式之美

設計模式之美 - 50 | 裝飾器模式:通過剖析Java IO類庫源碼學習裝飾器模式

上一節課我們學習了橋接模式,橋接模式有兩種理解方式。第一種理解方式是“將抽象和實現解耦,讓它們能獨立開發”。這種理解方式比較特別,應用場景也不多。另一種理解方式更加簡單,類似“組合優於繼承”設計原則,這種理解方式更加通用,應用場景比較多。不管是哪種理解方式,它們的代碼結構都是相同的,都是一種類之間的組合關係。

今天,我們通過剖析 Java IO 類的設計思想,再學習一種新的結構型模式,裝飾器模式。它的代碼結構跟橋接模式非常相似,不過,要解決的問題卻大不相同。

話不多說,讓我們正式開始今天的學習吧!

Java IO 類的“奇怪”用法

Java IO 類庫非常龐大和複雜,有幾十個類,負責 IO 數據的讀取和寫入。如果對 Java IO 類做一下分類,我們可以從下面兩個維度將它劃分爲四類。具體如下所示:
在這裏插入圖片描述
針對不同的讀取和寫入場景,Java IO 又在這四個父類基礎之上,擴展出了很多子類。具體如下所示:
在這裏插入圖片描述
在我初學 Java 的時候,曾經對 Java IO 的一些用法產生過很大疑惑,比如下面這樣一段代碼。我們打開文件 test.txt,從中讀取數據。其中,InputStream 是一個抽象類,FileInputStream 是專門用來讀取文件流的子類。BufferedInputStream 是一個支持帶緩存功能的數據讀取類,可以提高數據讀取的效率。

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
	//...
}

初看上面的代碼,我們會覺得 Java IO 的用法比較麻煩,需要先創建一個FileInputStream 對象,然後再傳遞給 BufferedInputStream 對象來使用。我在想,Java IO 爲什麼不設計一個繼承 FileInputStream 並且支持緩存的BufferedFileInputStream 類呢?這樣我們就可以像下面的代碼中這樣,直接創建一個BufferedFileInputStream 類對象,打開文件讀取數據,用起來豈不是更加簡單?

InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
	//...
}

基於繼承的設計方案

如果 InputStream 只有一個子類 FileInputStream 的話,那我們在 FileInputStream 基礎之上,再設計一個孫子類 BufferedFileInputStream,也算是可以接受的,畢竟繼承結構還算簡單。但實際上,繼承 InputStream 的子類有很多。我們需要給每一個InputStream 的子類,再繼續派生支持緩存讀取的子類。

除了支持緩存讀取之外,如果我們還需要對功能進行其他方面的增強,比如下面的DataInputStream 類,支持按照基本數據類型(int、boolean、long 等)來讀取數據。

FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();

在這種情況下,如果我們繼續按照繼承的方式來實現的話,就需要再繼續派生出DataFileInputStream、DataPipedInputStream 等類。如果我們還需要既支持緩存、又支持按照基本類型讀取數據的類,那就要再繼續派生出BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多類。這還只是附加了兩個增強功能,如果我們需要附加更多的增強功能,那就會導致組合爆炸,類繼承結構變得無比複雜,代碼既不好擴展,也不好維護。這也是我們在第 10 節中講的不推薦使用繼承的原因。

基於裝飾器模式的設計方案

在第 10 節中,我們還講到“組合優於繼承”,可以“使用組合來替代繼承”。針對剛剛的繼承結構過於複雜的問題,我們可以通過將繼承關係改爲組合關係來解決。下面的代碼展示了 Java IO 的這種設計思路。不過,我對代碼做了簡化,只抽象出了必要的代碼結構,如果你感興趣的話,可以直接去查看 JDK 源碼。

public abstract class InputStream {
	//...
	public int read(byte b[]) throws IOException {
		return read(b, 0, b.length);
	}
	
	public int read(byte b[], int off, int len) throws IOException {
		//...
	}
	
	public long skip(long n) throws IOException {
		//...
	}
	
	public int available() throws IOException {
		return 0;
	}
	
	public void close() throws IOException {}
	
	public synchronized void mark(int readlimit) {}
	
	public synchronized void reset() throws IOException {
		throw new IOException("mark/reset not supported");
	}
	
	public boolean markSupported() {
		return false;
	}
}

public class BufferedInputStream extends InputStream {
	protected volatile InputStream in;
	
	protected BufferedInputStream(InputStream in) {
		this.in = in;
	}
	
	//...實現基於緩存的讀數據接口...
}

public class DataInputStream extends InputStream {
	protected volatile InputStream in;
	
	protected DataInputStream(InputStream in) {
		this.in = in;
	}
	
	//...實現讀取基本類型數據的接口
}

看了上面的代碼,你可能會問,那裝飾器模式就是簡單的“用組合替代繼承”嗎?當然不是。從 Java IO 的設計來看,裝飾器模式相對於簡單的組合關係,還有兩個比較特殊的地方。

**第一個比較特殊的地方是:裝飾器類和原始類繼承同樣的父類,這樣我們可以對原始類“嵌套”多個裝飾器類。**比如,下面這樣一段代碼,我們對 FileInputStream 嵌套了兩個裝飾器類:BufferedInputStream 和 DataInputStream,讓它既支持緩存讀取,又支持按照基本數據類型來讀取數據。

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();

**第二個比較特殊的地方是:裝飾器類是對功能的增強,這也是裝飾器模式應用場景的一個重要特點。**實際上,符合“組合關係”這種代碼結構的設計模式有很多,比如之前講過的代理模式、橋接模式,還有現在的裝飾器模式。儘管它們的代碼結構很相似,但是每種設計模式的意圖是不同的。就拿比較相似的代理模式和裝飾器模式來說吧,代理模式中,代理類附加的是跟原始類無關的功能,而在裝飾器模式中,裝飾器類附加的是跟原始類相關的增強功能。

// 代理模式的代碼結構(下面的接口也可以替換成抽象類)
public interface IA {
	void f();
}
public class A impelements IA {
	public void f() { //... }
}
public class AProxy impements IA {
	private IA a;
	public AProxy(IA a) {
		this.a = a;
	}
	
	public void f() {
		// 新添加的代理邏輯
		a.f();
		// 新添加的代理邏輯
	}
}

// 裝飾器模式的代碼結構(下面的接口也可以替換成抽象類)
public interface IA {
	void f();
}
public class A impelements IA {
	public void f() { //... }
}
public class ADecorator impements IA {
	private IA a;
	public ADecorator(IA a) {
		this.a = a;
	}
	
	public void f() {
		// 功能增強代碼
		a.f();
		// 功能增強代碼
	}
}

實際上,如果去查看 JDK 的源碼,你會發現,BufferedInputStream、DataInputStream 並非繼承自 InputStream,而是另外一個叫 FilterInputStream 的類。那這又是出於什麼樣的設計意圖,才引入這樣一個類呢?

我們再重新來看一下 BufferedInputStream 類的代碼。InputStream 是一個抽象類而非接口,而且它的大部分函數(比如 read()、available())都有默認實現,按理來說,我們只需要在 BufferedInputStream 類中重新實現那些需要增加緩存功能的函數就可以了,其他函數繼承 InputStream 的默認實現。但實際上,這樣做是行不通的。

對於即便是不需要增加緩存功能的函數來說,BufferedInputStream 還是必須把它重新實現一遍,簡單包裹對 InputStream 對象的函數調用。具體的代碼示例如下所示。如果不重新實現,那 BufferedInputStream 類就無法將最終讀取數據的任務,委託給傳遞進來的 InputStream 對象來完成。這一部分稍微有點不好理解,你自己多思考一下。

public class BufferedInputStream extends InputStream {
	protected volatile InputStream in;
	
	protected BufferedInputStream(InputStream in) {
		this.in = in;
	}
	
	// f()函數不需要增強,只是重新調用一下InputStream in對象的f()
	public void f() {
		in.f();
	}
}

實際上,DataInputStream 也存在跟 BufferedInputStream 同樣的問題。爲了避免代碼重複,Java IO 抽象出了一個裝飾器父類 FilterInputStream,代碼實現如下所示。InputStream 的所有的裝飾器類(BufferedInputStream、DataInputStream)都繼承自這個裝飾器父類。這樣,裝飾器類只需要實現它需要增強的方法就可以了,其他方法繼承裝飾器父類的默認實現。

public class FilterInputStream extends InputStream {
	protected volatile InputStream in;
	
	protected FilterInputStream(InputStream in) {
		this.in = in;
	}
	
	public int read() throws IOException {
		return in.read();
	}
	
	public int read(byte b[]) throws IOException {
		return read(b, 0, b.length);
	}
	
	public int read(byte b[], int off, int len) throws IOException {
		return in.read(b, off, len);
	}
	
	public long skip(long n) throws IOException {
		return in.skip(n);
	}
	
	public int available() throws IOException {
		return in.available();
	}
	
	public void close() throws IOException {
		in.close();
	}
	
	public synchronized void mark(int readlimit) {
		in.mark(readlimit);
	}
	
	public synchronized void reset() throws IOException {
		in.reset();
	}
	
	public boolean markSupported() {
		return in.markSupported();
	}
}

重點回顧

好了,今天的內容到此就講完了。我們一塊來總結回顧一下,你需要重點掌握的內容。

裝飾器模式主要解決繼承關係過於複雜的問題,通過組合來替代繼承。它主要的作用是給原始類添加增強功能。這也是判斷是否該用裝飾器模式的一個重要的依據。除此之外,裝飾器模式還有一個特點,那就是可以對原始類嵌套使用多個裝飾器。爲了滿足這個應用場景,在設計的時候,裝飾器類需要跟原始類繼承相同的抽象類或者接口。

課堂討論

在上節課中,我們講到,可以通過代理模式給接口添加緩存功能。在這節課中,我們又通過裝飾者模式給 InputStream 添加緩存讀取數據功能。那對於“添加緩存”這個應用場景來說,我們到底是該用代理模式還是裝飾器模式呢?你怎麼看待這個問題?

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