摘要
本文描述了Decorator模式的三種不同實現:繼承、封裝與外掛。深入討論了每一種實現方式的優點與不足。
原文連結:Three approaches for decorating your code
每一們讀過GoF的那本著名的設計模式一書的人都會知道Decorator模式。現在,讓我們暫時
忘記所瞭解的Decorator概念,嘗試著從我們的開發經驗中去理解Decorator模式吧。
Decorator是用於裝飾一個事物(或人)的另一個事物(或人)。一個Decorator直接改變被
裝飾對象的職責或特徵,但是不能改變被裝飾對象的自有屬性。例如:一個鏡框可以裝飾圖
片,化妝品可以裝飾女孩的臉等等。
從我們的專業角度來討論一些存在的執行個體:
1 JScrollPane可以裝飾JComponent的視圖部分。JComponent本身並不會被改變,但是增加
了一個新的屬性(可滾動)。
2 BufferedInputStream是InputStream的裝飾子,本身BufferedInputStream就是一個
InputStream,但是它更快,因為提供了對資料的緩衝。
3 考慮一下DebugButton,它與JButton一樣,但是它在被點擊時可以向記錄檔添加訊息。
DebugButton是JButton的裝飾子,因為它直接改變了JButton但並沒有改變它的自有屬性。
4 再又如ScrollOverButton,它增加了一個滑鼠滑過的行為。當滑鼠移出時它是平的,當鼠
標經過時它具有一個凸起的邊框。很顯然,ScrollOverButton也是JButton的裝飾子。
現在,我們知道Decorator可能有三種不同的實現:
1 繼承(Inheritance)
2 封裝(Wrapper)
3 外掛(External)
本文將討論每一個實現模型,以及它們的優缺點。
繼承
對於開發人員而言,最直觀的Decorator實現就是:寫一個衍生類別,它繼承自被裝飾類,並
賦於新的職責。新的職責可以是通過增加方法或是修改已有方法來實現。
public class DebugButton extends JButton
{
public DebugButton()
{
addActionListener(new ActionListener()
{
System.out.println("debug message");
});
}
}
此外,我們也可以用相同的方式來實現ScrollOverButton:不是增加ActionListener,而是
增加MouseListener。在MouseListener回調方法中改變JButton的邊框,當mouseEntered()
被調用時,將邊框從EmpetyBorder變為RaisedBevelBorder。而當mouseExited()方法被調用
時,再將邊框從RaisedBevelBorder恢複成EmpetyBorder。
對於BufferedInputStream,同樣實現也是非常簡單的。修改每個讀資料的方法,讓它從內
存緩衝區來讀取資料。如果緩衝區是空的,它可以通過super.read()方法來擷取資料並填充
緩衝區。JScrollPane,要實現起來就有點複雜,下面我將討論為什麼它會比較難以用繼承的方式來實現。
討論一下繼承方式實現Decorator模式的優點與缺點:
優點
1 我們幾乎可以用這個方式實現所有的Decorator。
2 使用繼承方式實現Decorator模式,可以保留被裝飾類的原始類型,這一點是非常重要
的。用繼承方式,我們仍可以使用被裝飾類的在被裝飾之前的類型,例如,我們可以在我們
的應用程式中使用ScrollOverButton代替JButton,但是JScrollPane就不能代替包含在它內
部的對象。
缺點
1 用繼承的方式仍不夠直接。設想一下我們實現了ScrollOverButton和DebugButton,但是
我們又需要實現一個既有ScrollOverButton特點又有DebugButton行為的按鈕。怎麼辦?用
繼承方式我們唯一的選擇就是再派生出一個ScrollOverDebugButton類。如果我們有了
ScrollOverDebugButton的實現,那麼是否還需要繼續保留ScrollOverButton或DebugButton
實現?因為我們可以為ScrollOverDebugButton增加兩對方法來開啟或關閉debug或scroll-
over的行為:
public void setDebug(boolean b);
public boolean isDebug();
public void setScrollOver(boolean b);
public boolean isScrollOver();
再進一步考慮,如果將來我們有更多的裝飾功能,增加新的U1,U2,......Un個行為。我們是
不是要寫一個類,叫U1U2...UnButton?它是不是要包括2n個這樣的方法:
public void setU(boolean b);
public boolean getU;();
每增加一個新的行為(Un+1)給裝飾器就需要增加兩個新的方法,並要修改這個裝飾器的代
碼實現。這明顯與物件導向的思想相悖,可能會產生嚴重的後果。(注意:
javax.swing.JButton就是這樣實現的)。
2 多數可視化對象的行為是由風格參數來指定的,而風格的改變是不可預知的。當風格發生
了改變,我們不得不調整自己的改變。正如上面所述,使用繼承的方式可能需要改變實現的
代碼。
3 要保證被裝飾類的原始類型也不是一件容易的事。我們需要重載每個構造子,有時甚至是
靜態方式。儘管這不困難,但總是相當麻煩的一件事。
用繼承方式來實現Decorator模式並不象我們先前想像的那麼簡單。許多時候,我們並不知
道將來我們需要哪一些裝飾器,結果是,使用繼承方式的Decorator在擴充性方面相當困
難,並且與物件導向的原則會產生衝突。封裝(Wrapper)
封裝實現最主要的思想是將被裝飾的對象封裝入Decorator模式中。Decorator將外界請求轉
發給封裝的被裝飾對象,並且在轉寄之前(或之後)執行自己新增的功能,或者也可以提供
新的獨立方法來實現新增功能。
讓我們回到剛才的例子並且重新把它們用封裝方式來實現:
1 BufferedInputStream是一個InputStream的封裝,(關於這一點可以參考JDK中
java.io.BufferedInputStream類的說明或源碼)。儘管事實上BufferedInputStream也是
InputStream的一個衍生類別。作為封裝,在BufferedInputStream的構造子中擷取了另一個
InputStream對象,並且將它作為執行個體變數儲存起來,然後它可以轉寄請求到這個內建的
InputStream對象中。我們可以使用BufferedInputStream在我們原來使用InputStream場合。
2 JScrollPane也是一個封裝的實現。它轉寄請求到被封裝的組件中(它們被稱之為視
圖)。要注意的是,我們不能夠使用JScrollPane代替它內部的組件,因為它不支援所有的
視圖功能。例如,在JScrollPane的getFont()返回的是JScrollPane的字型而不是視圖的字型。
3 我們可以用這種方式實現DebugButton:
public class DebugButton extends JButton implements ActionListener
{
private JButton butt = null;
public DebugButton(JButton butt)
{
this.butt=butt;
butt.addActionListener(this);
}
// ActionListener
public void actionPerformed(ActionEvent e)
{
System.out.println("debug message for button" + butt);
}
. . . . . . . .
/* 需要提供約180個類似這樣的方法:
any JButton method M(params)
{
butt.M(params)
}
*/
這保持了被裝飾對象的類型(它繼承自JButton),但是這仍看上去不是那麼直接。
注意:我們不能夠使用java.lang.reflect.Proxy來作為代理,因為JButton是一個類而不是一個介面。
另一種實現可以這樣:
public class DebugButton extends JComponent implements ActionListener
{
private JButton butt = null;
public DebugButton(JButton butt)
{
this.butt=butt;
butt.addActionListener(this);
}
public JButton getUnderlineButton()
{
return butt;
}
// ActionListener
public void actionPerformed(ActionEvent e)
{
System.out.println("debug message for button" + butt);
}
. . . . . . . .
/*
可以實現一些(不多)可選的方法,象get/setFont,get/setBackground等。
*/
}
這個實現方式相當簡單,但這樣的DebugButton不是從JButton派生出來的,我們不可以用
DebugButton代替JButton。JScrollPane就是用這種方式實現的。
4 在ScrollOverButton也存在與DebugButton同樣的問題。從JButton派生則可能導致額外的
代碼,但可以保持JButton類型,如果從JComponent派生則可以更簡單和直接,但它不能保
持JButton類型。
也來討論一下封裝方式的優點與缺點:
優點
正如上文所述,用封裝實現Decorator可以減少所需要提供的方法,降低編碼量(象
InputStream)。所有的優點都可以歸結為這種實現方式可以得到短小精悍的類。
1 實現足夠簡單,並可以保持被封裝對象的類型
2 每個裝飾器獨立於其它裝飾器。
3 在許多場合,可以同時使用多個裝飾器。
缺點
然而,對於那些本身有眾多方法的類,使用封裝也會導致非常冗長的類代碼。對於可視化
的對象,我們需要提供上百個方法或是犧牲裝飾對象的類型。
根據GoF書中所言,封裝(Wrapper)才是真正意義上的裝飾器。它適用於代碼短小的被裝飾
類。對於長的類,開發人員不得不作出抉擇:是提供上百個方法以保持被裝飾對象的原有類
型?還是犧牲被裝飾對象的類型來換取簡單精鍊的代碼?外掛
為了描述這種外掛的實現方式,讓我們來看一下DebugButton和DebugDecorator類的實現代碼:
public class DebugDecorator implements ActionListener
{
public void decorateDebug(JButton butt)
{
butt.addActonListenr(this);
}
public void undecorateDebug(JButton butt)
{
butt.removeActonListenr(this);
}
// ActionListener
public void actionPerformed(ActionEvent evt)
{
JButton src = (JButton)evt.getSource();
System.out.println("debug message for button" + src);
}
}
方法decorateDebug()增加了一個ActionListener,方法undecorateDebug()則移除
ActionListener。方法actionPerformed()負責輸出debug資訊。
現在,看看如何使用上面的DebugDecorator類:
DebugDecorator decor = new DebugDecorator();
. . . . . . . .
JButton myButt = ...
. . . . . . . .
// Add external decorator
decor.decorateDebug(myButt);
. . . . . . . . .
// Remove external decorator
decor.undecorateDebug(myButt);
. . . . . . . . .
同樣的方式,我們可以實現RollOverDecorator類。在代碼中同時使用兩個裝飾器可以這樣:
DebugDecorator debugDecor = new DebugDecorator();
DebugDecorator rollDecor = new DebugDecorator();
. . . . . . . .
JButton myButt = ...
. . . . . . . .
// Add debug decorator
debugDecor.decorateDebug(myButt);
. . . . . . . .
// Add rollOver decorator
rollDecor.decorateRollOver(myButt);
. . . . . . . . .
. . . . . . . .
// Remove debug decorator
debugDecor.undecorateDebug(myButt);
. . . . . . . .
// Remove rollOver decorator
rollDecor.undecorateRollOver(myButt);
注意:在增加一個新的裝飾器就可以得到新的行為而不需要更改任何代碼。
我們可以應用一個DebugDecorator給任意多個JButton。從這點來說,在一個JVM中只需要一個DebugDecorator執行個體即足夠了,所以DebugDecorator可以實現為單體模式。
我把這種單體稱之為“單體裝飾器”,它可以(不是必須)有多於一個執行個體。而原則上的單體只能有一個執行個體。
現在看看重構的DebugDecorator:
public class DebugDecorator implements ActionListener
{
private static final DebugDecorator inst = new DebugDecorator();
public static getInstance()
{
return inst;
}
public void decorateDebug(JButton butt)
{
butt.addActonListenr(this);
}
public void undecorateDebug(JButton butt)
{
butt.removeActonListenr(this);
}
// ActionListener
public void actionPerformed(ActionEvent evt)
{
JButton src = (JButton)evt.getSource();
System.out.println("debug message for button" + src);
}
}
它的用法如下:
JButton myButt = ...
. . . . . . . .
// Add external decorator
DebugDecorator.getInstance().decorateDebug(myButt);
. . . . . . . . .
// Remove external decorator
DebugDecorator.getInstance().undecorateDebug(myButt);
. . . . . . . . .
再增加新的decorate()方法和undecoratedDebugContainer()方法:
public void decorateDebugContainer(JComponent container)
{
Component[] comps = container.getComponents();
for(int i = 0 ; i<comps.length; i++)
{
if(JButton.class.isInstance(comps[i]))
{
comps[i].addActionListener(this);
}
}
}
public void undecorateDebugContainer(JComponent container)
{
Component[] comps = container.getComponents();
for(int i = 0 ; i<comps.length; i++)
{
if(JButton.class.isInstance(comps[i]))
{
comps[i].removeActionListener(this);
}
}
}
這樣,我們就可以方便地將DebugDecorator應用於容器(例如ToolBar)了。
同樣我們也可以編寫出RollOverDecorator的實現,但是不能實現BufferedInputStream因為沒有合適的監聽器和回調方法。
再討論一下外掛方式的優點與缺點:
優點:
這種實現永遠不需要改變被裝飾對象的類型。
只要我們需要,就可以編寫任意多的裝飾器,並且可以任意地增加或移去它們。
每個裝飾器都是獨立於其它裝飾器。
在增加或移去裝飾器後,我們不需要改變任何代碼就可以得到新的功能。
這種方式可以便利地適應可視化對象的多變風格選擇。
缺點:
不幸的是,這種方式不能應用於任何對象的裝飾。它基於被裝飾對象的回調(如監聽器)和一些其它特性。換句話說,如果被裝飾對象沒有合適的特徵我們就不能應用外掛方式的裝飾器實現。
對於可視化對象,如果我們需要改變它的paint方法,則也不能使用外掛方式,比如,如果我們需要增加RoundButton。
雖然裝飾器的外掛實現是非常簡單,極易上手的,而且也符合物件導向的原則。但是它取決於被裝飾對象的許多特性,如監聽器、邊框,布局管理,以及可撥插的外觀風格。
總結
討論了三種不同類型的裝飾器實現,我們可以這樣比較它們:
我看不出在任何場合使用繼承來實現裝飾器是明智的。它看上去很簡單,但是在擴充性方面存在困難,並且違背了物件導向思想。建議不要使用這種方式來實現裝飾器。
封裝實現裝飾器的方式體現了良好的物件導向設計,也適用於那些方法不太多的類。只是對於一個很長的類,開發人員必須作出一個比較痛苦的選擇:是編寫大堆的代碼來保持被裝飾對象的原始類型?還是放棄它的原始類型為了得到精鍊的代碼?而且,對於可視化對象而言,這種方法在很多場合是不適用的。
外掛地實現裝飾器是便於使用的,也很好地體現了物件導向的思想。但是只能用於特定的類,它們需要提供一些特徵來支援外掛,如監聽器、邊框、布局管理器以及可撥插的外觀。對於可視化的對象,它可以工作地非常良好。