這系列相關部落格,參考 設計模式之美
設計模式之美 - 50 | 裝飾器模式:通過剖析Java IO類庫源碼學習裝飾器模式
- Java IO 類的“奇怪”用法
- 基于繼承的設計方案
- 基于裝飾器模式的設計方案
- 重點回顧
- 課堂讨論
上一節課我們學習了橋接模式,橋接模式有兩種了解方式。第一種了解方式是“将抽象和實作解耦,讓它們能獨立開發”。這種了解方式比較特别,應用場景也不多。另一種了解方式更加簡單,類似“組合優于繼承”設計原則,這種了解方式更加通用,應用場景比較多。不管是哪種了解方式,它們的代碼結構都是相同的,都是一種類之間的組合關系。
今天,我們通過剖析 Java IO 類的設計思想,再學習一種新的結構型模式,裝飾器模式。它的代碼結構跟橋接模式非常相似,不過,要解決的問題卻大不相同。
話不多說,讓我們正式開始今天的學習吧!
Java IO 類的“奇怪”用法
Java IO 類庫非常龐大和複雜,有幾十個類,負責 IO 資料的讀取和寫入。如果對 Java IO 類做一下分類,我們可以從下面兩個次元将它劃分為四類。具體如下所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL9kEWadnTYlVc5YVWvZkMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLzgzM3UDN1QDMxETNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
針對不同的讀取和寫入場景,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 添加緩存讀取資料功能。那對于“添加緩存”這個應用場景來說,我們到底是該用代理模式還是裝飾器模式呢?你怎麼看待這個問題?