天天看點

設計模式之美 - 50 | 裝飾器模式:通過剖析Java IO類庫源碼學習裝飾器模式Java IO 類的“奇怪”用法基于繼承的設計方案基于裝飾器模式的設計方案重點回顧課堂讨論

這系列相關部落格,參考 設計模式之美

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

  • Java IO 類的“奇怪”用法
  • 基于繼承的設計方案
  • 基于裝飾器模式的設計方案
  • 重點回顧
  • 課堂讨論

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

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

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

Java IO 類的“奇怪”用法

Java IO 類庫非常龐大和複雜,有幾十個類,負責 IO 資料的讀取和寫入。如果對 Java IO 類做一下分類,我們可以從下面兩個次元将它劃分為四類。具體如下所示:

設計模式之美 - 50 | 裝飾器模式:通過剖析Java IO類庫源碼學習裝飾器模式Java IO 類的“奇怪”用法基于繼承的設計方案基于裝飾器模式的設計方案重點回顧課堂讨論

針對不同的讀取和寫入場景,Java IO 又在這四個父類基礎之上,擴充出了很多子類。具體如下所示:

設計模式之美 - 50 | 裝飾器模式:通過剖析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 添加緩存讀取資料功能。那對于“添加緩存”這個應用場景來說,我們到底是該用代理模式還是裝飾器模式呢?你怎麼看待這個問題?

繼續閱讀