【學習難度:★★★☆☆,使用頻率:★★★☆☆】
直接出處:裝飾模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/14
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed
擴展系統(tǒng)功能——裝飾模式(一)
盡管目前房價依舊很高,但還是阻止不了大家對新房的渴望和買房的熱情。如果大家買的是毛坯房,無疑還有一項艱巨的任務要面對,那就是裝修。對新房進行裝修并沒有改變房屋用于居住的本質,但它可以讓房子變得更漂亮、更溫馨、更實用、更能滿足居家的需求。在軟件設計中,我們也有一種類似新房裝修的技術可以對已有對象(新房)的功能進行擴展(裝修),以獲得更加符合用戶需求的對象,使得對象具有更加強大的功能。這種技術對應于一種被稱之為裝飾模式的設計模式,本章將介紹用于擴展系統(tǒng)功能的裝飾模式。
12.1 圖形界面構件庫的設計
Sunny軟件公司基于面向對象技術開發(fā)了一套圖形界面構件庫VisualComponent,該構件庫提供了大量基本構件,如窗體、文本框、列表框等,由于在使用該構件庫時,用戶經(jīng)常要求定制一些特效顯示效果,如帶滾動條的窗體、帶黑色邊框的文本框、既帶滾動條又帶黑色邊框的列表框等等,因此經(jīng)常需要對該構件庫進行擴展以增強其功能,如圖12-1所示:
如何提高圖形界面構件庫性的可擴展性并降低其維護成本是Sunny公司開發(fā)人員必須面對的一個問題。
Sunny軟件公司的開發(fā)人員針對上述要求,提出了一個基于繼承復用的初始設計方案,其基本結構如圖12-2所示:
圖12-2中,在抽象類Component中聲明了抽象方法display(),其子類Window、TextBox等實現(xiàn)了display()方法,可以顯示最簡單的控件,再通過它們的子類來對功能進行擴展,例如,在Window的子類ScrollBarWindow、BlackBorderWindow中對Window中的display()方法進行擴展,分別實現(xiàn)帶滾動條和帶黑色邊框的窗體。仔細分析該設計方案,我們不難發(fā)現(xiàn)存在如下幾個問題:
(1) 系統(tǒng)擴展麻煩,在某些編程語言中無法實現(xiàn)。如果用戶需要一個既帶滾動條又帶黑色邊框的窗體,在圖12-2中通過增加了一個新的類ScrollBarAndBlackBorderWindow來實現(xiàn),該類既作為ScrollBarWindow的子類,又作為BlackBorderWindow的子類;但現(xiàn)在很多面向對象編程語言,如Java、C#等都不支持多重類繼承,因此在這些語言中無法通過繼承來實現(xiàn)對來自多個父類的方法的重用。此外,如果還需要擴展一項功能,例如增加一個透明窗體類TransparentWindow,它是Window類的子類,可以將一個窗體設置為透明窗體,現(xiàn)在需要一個同時擁有三項功能(帶滾動條、帶黑色邊框、透明)的窗體,必須再增加一個類作為三個窗體類的子類,這同樣在Java等語言中無法實現(xiàn)。系統(tǒng)在擴展時非常麻煩,有時候甚至無法實現(xiàn)。
(2)代碼重復。從圖12-2中我們可以看出,不只是窗體需要設置滾動條,文本框、列表框等都需要設置滾動條,因此在ScrollBarWindow、ScrollBarTextBox和ScrollBarListBox等類中都包含用于增加滾動條的方法setScrollBar(),該方法的具體實現(xiàn)過程基本相同,代碼重復,不利于對系統(tǒng)進行修改和維護。
(3) 系統(tǒng)龐大,類的數(shù)目非常多。如果增加新的控件或者新的擴展功能系統(tǒng)都需要增加大量的具體類,這將導致系統(tǒng)變得非常龐大。在圖12-2中,3種基本控件和2種擴展方式需要定義9個具體類;如果再增加一個基本控件還需要增加3個具體類;增加一種擴展方式則需要增加更多的類,如果存在3種擴展方式,對于每一個控件而言,需要增加7個具體類,因為這3種擴展方式存在7種組合關系(大家自己分析為什么需要7個類?)。
總之,圖12-2不是一個好的設計方案,怎么辦?如何讓系統(tǒng)中的類可以進行擴展但是又不會導致類數(shù)目的急劇增加?不用著急,讓我們先來分析為什么這個設計方案會存在如此多的問題。根本原因在于復用機制的不合理,圖12-2采用了繼承復用,例如在ScrollBarWindow中需要復用Window類中定義的display()方法,同時又增加新的方法setScrollBar(),ScrollBarTextBox和ScrollBarListBox都必須做類似的處理,在復用父類的方法后再增加新的方法來擴展功能。根據(jù)“合成復用原則”,在實現(xiàn)功能復用時,我們要多用關聯(lián),少用繼承,因此我們可以換個角度來考慮,將setScrollBar()方法抽取出來,封裝在一個獨立的類中,在這個類中定義一個Component類型的對象,通過調用Component的display()方法來顯示最基本的構件,同時再通過setScrollBar()方法對基本構件的功能進行增強。由于Window、ListBox和TextBox都是Component的子類,根據(jù)“里氏代換原則”,程序在運行時,我們只要向這個獨立的類中注入具體的Component子類的對象即可實現(xiàn)功能的擴展。這個獨立的類一般稱為裝飾器(Decorator)或裝飾類,顧名思義,它的作用就是對原有對象進行裝飾,通過裝飾來擴展原有對象的功能。
裝飾類的引入將大大簡化本系統(tǒng)的設計,它也是裝飾模式的核心,下面讓我們正式進入裝飾模式的學習。
擴展系統(tǒng)功能——裝飾模式(二)
12.2 裝飾模式概述
裝飾模式可以在不改變一個對象本身功能的基礎上給對象增加額外的新行為,在現(xiàn)實生活中,這種情況也到處存在,例如一張照片,我們可以不改變照片本身,給它增加一個相框,使得它具有防潮的功能,而且用戶可以根據(jù)需要給它增加不同類型的相框,甚至可以在一個小相框的外面再套一個大相框。
裝飾模式是一種用于替代繼承的技術,它通過一種無須定義子類的方式來給對象動態(tài)增加職責,使用對象之間的關聯(lián)關系取代類之間的繼承關系。在裝飾模式中引入了裝飾類,在裝飾類中既可以調用待裝飾的原有類的方法,還可以增加新的方法,以擴充原有類的功能。
裝飾模式定義如下:
裝飾模式(Decorator Pattern):動態(tài)地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生成子類實現(xiàn)更為靈活。裝飾模式是一種對象結構型模式。
在裝飾模式中,為了讓系統(tǒng)具有更好的靈活性和可擴展性,我們通常會定義一個抽象裝飾類,而將具體的裝飾類作為它的子類,裝飾模式結構如圖12-3所示:
在裝飾模式結構圖中包含如下幾個角色:
Component(抽象構件):它是具體構件和抽象裝飾類的共同父類,聲明了在具體構件中實現(xiàn)的業(yè)務方法,它的引入可以使客戶端以一致的方式處理未被裝飾的對象以及裝飾之后的對象,實現(xiàn)客戶端的透明操作。
ConcreteComponent(具體構件):它是抽象構件類的子類,用于定義具體的構件對象,實現(xiàn)了在抽象構件中聲明的方法,裝飾器可以給它增加額外的職責(方法)。
Decorator(抽象裝飾類):它也是抽象構件類的子類,用于給具體構件增加職責,但是具體職責在其子類中實現(xiàn)。它維護一個指向抽象構件對象的引用,通過該引用可以調用裝飾之前構件對象的方法,并通過其子類擴展該方法,以達到裝飾的目的。
ConcreteDecorator(具體裝飾類):它是抽象裝飾類的子類,負責向構件添加新的職責。每一個具體裝飾類都定義了一些新的行為,它可以調用在抽象裝飾類中定義的方法,并可以增加新的方法用以擴充對象的行為。
由于具體構件類和裝飾類都實現(xiàn)了相同的抽象構件接口,因此裝飾模式以對客戶透明的方式動態(tài)地給一個對象附加上更多的責任,換言之,客戶端并不會覺得對象在裝飾前和裝飾后有什么不同。裝飾模式可以在不需要創(chuàng)造更多子類的情況下,將對象的功能加以擴展。
裝飾模式的核心在于抽象裝飾類的設計,其典型代碼如下所示:
class Decorator implements Component
{
private Component component; //維持一個對抽象構件對象的引用
public Decorator(Component component) //注入一個抽象構件類型的對象
{
this.component=component;
}
public void operation()
{
component.operation(); //調用原有業(yè)務方法
}
}
在抽象裝飾類Decorator中定義了一個Component類型的對象component,維持一個對抽象構件對象的引用,并可以通過構造方法或Setter方法將一個Component類型的對象注入進來,同時由于Decorator類實現(xiàn)了抽象構件Component接口,因此需要實現(xiàn)在其中聲明的業(yè)務方法operation(),需要注意的是在Decorator中并未真正實現(xiàn)operation()方法,而只是調用原有component對象的operation()方法,它沒有真正實施裝飾,而是提供一個統(tǒng)一的接口,將具體裝飾過程交給子類完成。
在Decorator的子類即具體裝飾類中將繼承operation()方法并根據(jù)需要進行擴展,典型的具體裝飾類代碼如下:
class ConcreteDecorator extends Decorator
{
public ConcreteDecorator(Component component)
{
super(component);
}
public void operation()
{
super.operation(); //調用原有業(yè)務方法
addedBehavior(); //調用新增業(yè)務方法
}
//新增業(yè)務方法
public void addedBehavior()
{
……
}
}
在具體裝飾類中可以調用到抽象裝飾類的operation()方法,同時可以定義新的業(yè)務方法,如addedBehavior()。
由于在抽象裝飾類Decorator中注入的是Component類型的對象,因此我們可以將一個具體構件對象注入其中,再通過具體裝飾類來進行裝飾;此外,我們還可以將一個已經(jīng)裝飾過的Decorator子類的對象再注入其中進行多次裝飾,從而對原有功能的多次擴展。
思考
能否在裝飾模式中找出兩個獨立變化的維度?試比較裝飾模式和橋接模式的相同之處和不同之處?
擴展系統(tǒng)功能——裝飾模式(三)
12.3 完整解決方案
為了讓系統(tǒng)具有更好的靈活性和可擴展性,克服繼承復用所帶來的問題,Sunny公司開發(fā)人員使用裝飾模式來重構圖形界面構件庫的設計,其中部分類的基本結構如圖12-4所示:
在圖12-4中,Component充當抽象構件類,其子類Window、TextBox、ListBox充當具體構件類,Component類的另一個子類ComponentDecorator充當抽象裝飾類,ComponentDecorator的子類ScrollBarDecorator和BlackBorderDecorator充當具體裝飾類。完整代碼如下所示:
//抽象界面構件類:抽象構件類,為了突出與模式相關的核心代碼,對原有控件代碼進行了大量的簡化
abstract class Component
{
public abstract void display();
}
//窗體類:具體構件類
class Window extends Component
{
public void display()
{
System.out.println("顯示窗體!");
}
}
//文本框類:具體構件類
class TextBox extends Component
{
public void display()
{
System.out.println("顯示文本框!");
}
}
//列表框類:具體構件類
class ListBox extends Component
{
public void display()
{
System.out.println("顯示列表框!");
}
}
//構件裝飾類:抽象裝飾類
class ComponentDecorator extends Component
{
private Component component; //維持對抽象構件類型對象的引用
public ComponentDecorator(Component component) //注入抽象構件類型的對象
{
this.component = component;
}
public void display()
{
component.display();
}
}
//滾動條裝飾類:具體裝飾類
class ScrollBarDecorator extends ComponentDecorator
{
public ScrollBarDecorator(Component component)
{
super(component);
}
public void display()
{
this.setScrollBar();
super.display();
}
public void setScrollBar()
{
System.out.println("為構件增加滾動條!");
}
}
//黑色邊框裝飾類:具體裝飾類
class BlackBorderDecorator extends ComponentDecorator
{
public BlackBorderDecorator(Component component)
{
super(component);
}
public void display()
{
this.setBlackBorder();
super.display();
}
public void setBlackBorder()
{
System.out.println("為構件增加黑色邊框!");
}
}
編寫如下客戶端測試代碼:
class Client
{
public static void main(String args[])
{
Component component,componentSB; //使用抽象構件定義
component = new Window(); //定義具體構件
componentSB = new ScrollBarDecorator(component); //定義裝飾后的構件
componentSB.display();
}
}
編譯并運行程序,輸出結果如下:
為構件增加滾動條!
顯示窗體!
在客戶端代碼中,我們先定義了一個Window類型的具體構件對象component,然后將component作為構造函數(shù)的參數(shù)注入到具體裝飾類ScrollBarDecorator中,得到一個裝飾之后對象componentSB,再調用componentSB的display()方法后將得到一個有滾動條的窗體。如果我們希望得到一個既有滾動條又有黑色邊框的窗體,不需要對原有類庫進行任何修改,只需將客戶端代碼修改為如下所示:
class Client
{
public static void main(String args[])
{
Component component,componentSB,componentBB; //全部使用抽象構件定義
component = new Window();
componentSB = new ScrollBarDecorator(component);
componentBB = new BlackBorderDecorator(componentSB); //將裝飾了一次之后的對象繼續(xù)注入到另一個裝飾類中,進行第二次裝飾
componentBB.display();
}
}
編譯并運行程序,輸出結果如下:
為構件增加黑色邊框!
為構件增加滾動條!
顯示窗體!
我們可以將裝飾了一次之后的componentSB對象注入另一個裝飾類BlackBorderDecorator中實現(xiàn)第二次裝飾,得到一個經(jīng)過兩次裝飾的對象componentBB,再調用componentBB的display()方法即可得到一個既有滾動條又有黑色邊框的窗體。
如果需要在原有系統(tǒng)中增加一個新的具體構件類或者新的具體裝飾類,無須修改現(xiàn)有類庫代碼,只需將它們分別作為抽象構件類或者抽象裝飾類的子類即可。與圖12-2所示的繼承結構相比,使用裝飾模式之后將大大減少了子類的個數(shù),讓系統(tǒng)擴展起來更加方便,而且更容易維護,是取代繼承復用的有效方式之一。
擴展系統(tǒng)功能——裝飾模式(四)
12.4 透明裝飾模式與半透明裝飾模式
裝飾模式雖好,但存在一個問題。如果客戶端希望單獨調用具體裝飾類新增的方法,而不想通過抽象構件中聲明的方法來調用新增方法時將遇到一些麻煩,我們通過一個實例來對這種情況加以說明:
在Sunny軟件公司開發(fā)的Sunny OA系統(tǒng)中,采購單(PurchaseRequest)和請假條(LeaveRequest)等文件(Document)對象都具有顯示功能,現(xiàn)在要為其增加審批、刪除等功能,使用裝飾模式進行設計。
我們使用裝飾模式可以得到如圖12-5所示結構圖:
在圖12-5中,Document充當抽象構件類,PurchaseRequest和LeaveRequest充當具體構件類,Decorator充當抽象裝飾類,Approver和Deleter充當具體裝飾類。其中Decorator類和Approver類的示例代碼如下所示:
//抽象裝飾類
class Decorator implements Document
{
private Document document;
public Decorator(Document document)
{
this. document = document;
}
public void display()
{
document.display();
}
}
//具體裝飾類
class Approver extends Decorator
{
public Approver(Document document)
{
super(document);
System.out.println("增加審批功能!");
}
public void approve()
{
System.out.println("審批文件!");
}
}
大家注意,Approver類繼承了抽象裝飾類Decorator的display()方法,同時新增了業(yè)務方法approve(),但這兩個方法是獨立的,沒有任何調用關系。如果客戶端需要分別調用這兩個方法,代碼片段如下所示:
Document doc; //使用抽象構件類型定義
doc = new PurchaseRequest();
Approver newDoc; //使用具體裝飾類型定義
newDoc = new Approver(doc);
newDoc.display();//調用原有業(yè)務方法
newDoc.approve();//調用新增業(yè)務方法
如果newDoc也使用Document類型來定義,將導致客戶端無法調用新增業(yè)務方法approve(),因為在抽象構件類Document中沒有對approve()方法的聲明。也就是說,在客戶端無法統(tǒng)一對待裝飾之前的具體構件對象和裝飾之后的構件對象。
在實際使用過程中,由于新增行為可能需要單獨調用,因此這種形式的裝飾模式也經(jīng)常出現(xiàn),這種裝飾模式被稱為半透明(Semi-transparent)裝飾模式,而標準的裝飾模式是透明(Transparent)裝飾模式。下面我們對這兩種裝飾模式進行較為詳細的介紹:
(1)透明裝飾模式
在透明裝飾模式中,要求客戶端完全針對抽象編程,裝飾模式的透明性要求客戶端程序不應該將對象聲明為具體構件類型或具體裝飾類型,而應該全部聲明為抽象構件類型。對于客戶端而言,具體構件對象和具體裝飾對象沒有任何區(qū)別。也就是應該使用如下代碼:
Component c, c1; //使用抽象構件類型定義對象
c = new ConcreteComponent();
c1 = new ConcreteDecorator (c);
而不應該使用如下代碼:
ConcreteComponent c; //使用具體構件類型定義對象
c = new ConcreteComponent();
或
ConcreteDecorator c1; //使用具體裝飾類型定義對象
c1 = new ConcreteDecorator(c);
在12.3節(jié)圖形界面構件庫的設計方案中使用的就是透明裝飾模式,在客戶端中存在如下代碼片段:
……
Component component,componentSB,componentBB; //全部使用抽象構件定義
component = new Window();
componentSB = new ScrollBarDecorator(component);
componentBB = new BlackBorderDecorator(componentSB);
componentBB.display();
……
使用抽象構件類型Component定義全部具體構件對象和具體裝飾對象,客戶端可以一致地使用這些對象,因此符合透明裝飾模式的要求。
透明裝飾模式可以讓客戶端透明地使用裝飾之前的對象和裝飾之后的對象,無須關心它們的區(qū)別,此外,還可以對一個已裝飾過的對象進行多次裝飾,得到更為復雜、功能更為強大的對象。在實現(xiàn)透明裝飾模式時,要求具體裝飾類的operation()方法覆蓋抽象裝飾類的operation()方法,除了調用原有對象的operation()外還需要調用新增的addedBehavior()方法來增加新行為
(2)半透明裝飾模式
透明裝飾模式的設計難度較大,而且有時我們需要單獨調用新增的業(yè)務方法。為了能夠調用到新增方法,我們不得不用具體裝飾類型來定義裝飾之后的對象,而具體構件類型還是可以使用抽象構件類型來定義,這種裝飾模式即為半透明裝飾模式,也就是說,對于客戶端而言,具體構件類型無須關心,是透明的;但是具體裝飾類型必須指定,這是不透明的。如本節(jié)前面所提到的文件對象功能增加實例,為了能夠調用到在Approver中新增方法approve(),客戶端代碼片段如下所示:
……
Document doc; //使用抽象構件類型定義
doc = new PurchaseRequest();
Approver newDoc; //使用具體裝飾類型定義
newDoc = new Approver(doc);
……
半透明裝飾模式可以給系統(tǒng)帶來更多的靈活性,設計相對簡單,使用起來也非常方便;但是其最大的缺點在于不能實現(xiàn)對同一個對象的多次裝飾,而且客戶端需要有區(qū)別地對待裝飾之前的對象和裝飾之后的對象。在實現(xiàn)半透明的裝飾模式時,我們只需在具體裝飾類中增加一個獨立的addedBehavior()方法來封裝相應的業(yè)務處理,由于客戶端使用具體裝飾類型來定義裝飾后的對象,因此可以單獨調用addedBehavior()方法來擴展系統(tǒng)功能。
思考
為什么半透明裝飾模式不能實現(xiàn)對同一個對象的多次裝飾?
12.5 裝飾模式注意事項
在使用裝飾模式時,通常我們需要注意以下幾個問題:
(1) 盡量保持裝飾類的接口與被裝飾類的接口相同,這樣,對于客戶端而言,無論是裝飾之前的對象還是裝飾之后的對象都可以一致對待。這也就是說,在可能的情況下,我們應該盡量使用透明裝飾模式。
(2) 盡量保持具體構件類ConcreteComponent是一個“輕”類,也就是說不要把太多的行為放在具體構件類中,我們可以通過裝飾類對其進行擴展。
(3) 如果只有一個具體構件類,那么抽象裝飾類可以作為該具體構件類的直接子類。如圖12-6所示:
12.6 裝飾模式總結
裝飾模式降低了系統(tǒng)的耦合度,可以動態(tài)增加或刪除對象的職責,并使得需要裝飾的具體構件類和具體裝飾類可以獨立變化,以便增加新的具體構件類和具體裝飾類。在軟件開發(fā)中,裝飾模式應用較為廣泛,例如在JavaIO中的輸入流和輸出流的設計、javax.swing包中一些圖形界面構件功能的增強等地方都運用了裝飾模式。
- 主要優(yōu)點
裝飾模式的主要優(yōu)點如下:
(1) 對于擴展一個對象的功能,裝飾模式比繼承更加靈活性,不會導致類的個數(shù)急劇增加。
(2) 可以通過一種動態(tài)的方式來擴展一個對象的功能,通過配置文件可以在運行時選擇不同的具體裝飾類,從而實現(xiàn)不同的行為。
(3) 可以對一個對象進行多次裝飾,通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創(chuàng)造出很多不同行為的組合,得到功能更為強大的對象。
(4) 具體構件類與具體裝飾類可以獨立變化,用戶可以根據(jù)需要增加新的具體構件類和具體裝飾類,原有類庫代碼無須改變,符合“開閉原則”。
- 主要缺點
裝飾模式的主要缺點如下:
(1) 使用裝飾模式進行系統(tǒng)設計時將產(chǎn)生很多小對象,這些對象的區(qū)別在于它們之間相互連接的方式有所不同,而不是它們的類或者屬性值有所不同,大量小對象的產(chǎn)生勢必會占用更多的系統(tǒng)資源,在一定程序上影響程序的性能。
(2) 裝飾模式提供了一種比繼承更加靈活機動的解決方案,但同時也意味著比繼承更加易于出錯,排錯也很困難,對于多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較為繁瑣。
- 適用場景
在以下情況下可以考慮使用裝飾模式:
(1) 在不影響其他對象的情況下,以動態(tài)、透明的方式給單個對象添加職責。
(2) 當不能采用繼承的方式對系統(tǒng)進行擴展或者采用繼承不利于系統(tǒng)擴展和維護時可以使用裝飾模式。不能采用繼承的情況主要有兩類:第一類是系統(tǒng)中存在大量獨立的擴展,為支持每一種擴展或者擴展之間的組合將產(chǎn)生大量的子類,使得子類數(shù)目呈爆炸性增長;第二類是因為類已定義為不能被繼承(如Java語言中的final類)。
練習
Sunny軟件公司欲開發(fā)了一個數(shù)據(jù)加密模塊,可以對字符串進行加密。最簡單的加密算法通過對字母進行移位來實現(xiàn),同時還提供了稍復雜的逆向輸出加密,還提供了更為高級的求模加密。用戶先使用最簡單的加密算法對字符串進行加密,如果覺得還不夠可以對加密之后的結果使用其他加密算法進行二次加密,當然也可以進行第三次加密。試使用裝飾模式設計該多重加密系統(tǒng)。
練習會在我的github上做掉