在這主題的最後一回,我們要介紹另一個有趣的模仿型物件Decorator。它是個設計模式的名稱,有時稱為Wrapper。每個Decorator都對應了本尊物件,而身為模仿型物件的它,自然也和這本尊物件有相同介面。當客戶端程式碼呼叫Decorator承襲自本尊物件介面中的函式時,Decorator物件最終也是呼叫本尊物件介面中的同名函式,但Decorator會先有額外的加工,這就是所謂的「裝飾」,也是名稱的來由。

運用Decorator的最佳範例
在我所見過運用Decorator最巧妙的,就是Java 的I/O程式庫。當中有兩個用來處理串流的類別:InputStream和OutputStream。這兩個都是抽象類別,所以各自都有衍生的實作類別。例如FileInputStream用來讀取檔案的內容,而FileOutputStream則可用來將內容寫至檔案;ByteArrayInputStream可用來以串流的方式讀取byte陣列的內容,ByteArrayOutputStream則可以串流方式將內容寫至byte陣列。

不論是FileInputStream、ByteArrayInputStream,都是實際從某個來源讀出資料的InputStream實作,FileOutputStream、ByteArrayOutputStream也都是實際將資料寫至某個目標的OutputStream實作。在Java的I/O程式庫中,也有類似的InputStream或OutputStream,都是實際執行讀取或寫入動作的類別。不過,還有一種類型是不實際執行讀取或寫入動作的,它們是在讀取後、或寫入前做額外加工。

攤開Java的I/O程式庫,這樣的類別有那些?

例如ObjectInputStream和ObjectOutputStream,它們並非將資料自某個載體讀出,或是將資料寫至某個載體,你沒有辦法憑藉它們做到這件事,在建構它們時,你得傳入另一個InputStream或OutputStream,最終它會將資料自你傳入的InputStream讀出,或者將資料寫入至你傳入的OutputStream去。但它讀取該InputStream、或對該OutputStream寫入時,它會進行一些額外的加工。對ObjectInputStream而言,這加工,便是在讀取資料後,做物件的解序列化(de-serialize),而對ObjectOutputStream而言,額外的加工便是在寫入資料之前,先進行物件資料的序列化(serialize)。

這麼一來,當對ObjectInputStream做讀取物件的動作時(readObject()),它便會自底層的InputStream(也就是建構ObjectInputStream時所傳入的InputStream)讀取資料,倘若該資料是物件序列化後的資料,經由ObjectInputStream的處理,自然而然就完成了解序列化的動作。同樣的,當對ObjectOutputStream做寫入物件的動作時(writeObject()),便會自動將物件的資料進行序列化的處理,接著才將資料,寫至底層的OutputStream(也就是建構ObjectOutputStream時所傳入的OutputStream)。

從上述你可以觀察到,像ObjectInputStream/ObjectOutputStream都不負責實際載體的讀取或寫入,真的負責的,都是建構時所傳入的InputStream/OutputStream物件,但ObjectInputStream/ObjectOutputStream和InputStream/OutputStream有相同的介面(它們都是InputStream/OutputStream的衍生類別),這代表著,它們都模仿著本尊物件(也就是建構時所傳入的InputStream/OutputStream)的介面。

經由Decorator的介面來加工的意義與目的
客戶端程式碼在使用時,基本上是透過Decorator物件的介面,而不透過本尊物件,因為這些程式碼希望最後的效果,是經由Decorator加工的。例如:

FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();

客戶端程式碼雖然建立了FileInputStream物件,卻不直接使用它,反倒是再把建立出來的FileInputStream拿去建立另一個ObjectInputStream,接著才透過ObjectInputStream讀到自己想要的東西,因為經過ObjectInputStream加工後的結果,才是客戶端程式碼想要的東西──從檔案讀取物件序列化後的資料,然後進行解序列化、還原成物件。客戶端程式碼可以用同樣的模式,套用在FileOutputStream和ObjectOutputStream上,將物件資料序列化後,寫到檔案中。

Java的程式庫中有不少這樣的例子,像GZIPInputStream/GZIPOutputStream,可以在底層的InputStream/OutputStream之上,提供GZIP壓縮及解壓縮的加工。各種Decorator,都有不同的加工用途。

有趣的地方在於,GZIPInputStream/GZIPOutputStream可以搭配FileInputStream/FileOutputStream來使用,使得它們的額外加工是作用在檔案之上,它們同樣也可搭配ByteArrayInputStream/ByteArrayOutputStream來作用在byte陣列。更重要的是,它們可搭配任意的InputStream/OutputStream,將加工作用在任意的InputStream/OutputStream。你可能希望透過網路傳輸時,順便執行GZIP壓縮,那麼就可以傳入SocketOutputStream物件至GZIPOutputStream的建構式中,而當你對GZIPOutputStream寫入資料時,資料會先被GZIPOutputStream壓縮後,才會由底層的SocketOutputStream傳送到網路上。

不論底層的InputStream/OutputStream是什麼,GZIPInputStream/GZIPOutputStream都能對它們加工,方式也都一樣,這正是Decorator模式的威力。同樣的,不論底層的InputStream/OutputStream是什麼,ObjectInputStream/ObjectOutputStream也都能對它們加工(物件的序列化及解序列化)。

我們可透過不同的Decorator來動態、彈性搭配,選擇對底層本尊物件的處理,這就是好處。

可遞迴執行,連續處理多個程序
另一點更重要,Decorator具有像數學上函式的特性──f(x)是個函式,g(x)也是,我們可以透過g(f(x))得到兩個函式先後作用的結果。例如:

FileOutputStream fos = new FileOutputStream(filename);
GZIPOutputStream gos = new GZIPOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(gos);
oos.writeObject(obj);

我們先建出最底層能執行檔案寫入動作的FileOutputStream,接著在FileOutputStream之上建出了GZIPOutputStream,這意謂著當我們對GZIPOutputStream寫入時,資料會先經GZIP壓縮再寫到檔案中。但這還不夠,最後我們在GZIPOutputStream之上,再建出ObjectOutputStream。當我們對ObjectOutputStream寫入物件時,你可以想像:資料會先做物件的序列化處理,接著再執行GZIP壓縮,最後才寫到檔案中。

對客戶端的程式碼而言,物件的序列化、GZIP的壓縮、檔案的寫入,可以說是在一次寫入動作裡三效合一,好比我們把三個函式合成在一塊。

事實上,你可疊上任意個數的InputStream或OutputStream Decorator,以同時取得綜效。這背後,當然是因每個Decorator都有InputStream或OutputStream的介面,而它們也都接受傳入InputStream或OutputStream做為底層的本尊物件,所以它們可遞迴般的,無限繼續疊下去。

將每種功能都寫成Decorator,使得客戶端程式碼可動態選擇Decorator,來達成不同作用,而又因每種Decorator介面相同,使它可層層套用,讓客戶端程式碼能同時取得不同效果,正是這設計模式巧妙之處。

作者簡介


Advertisement

更多 iThome相關內容