設計模式(六):裝飾器模式

赫連小伍發表於2021-10-11

俄羅斯套娃大家都玩過吧,就像是這樣

這玩意玩起來很上頭,開啟一個總期待會有下一個,充滿了趣味性

程式設計師在寫程式碼時,也會遇到像套娃這樣令人上頭的程式碼

開啟一個類,裡面還有一個類,再開啟一個,裡面還有一個...

這種套娃似的程式碼其實是一種很常見的設計模式,它叫裝飾器模式

今天我們就來扒一扒裝飾器模式到底是個什麼東西

實際案例

假如我們要寫一個支付的功能,支付的方式有支付寶和微信

我們用程式碼來實現這個功能

首先我們要定義一個介面類Payment,這個介面類用來規定支付功能應該有哪些行為,也就是應該有哪些方法

比如可以有支付查詢兩個方法,對應的方法名分別是pay()query()

然後分別定義一個支付寶類和微信類實現Payment介面類,並重寫支付查詢的方法

這樣,支付功能就寫好了,在需要支付的時候直接呼叫就可以了

比如,需要使用支付寶支付時,可以這樣呼叫

Payment payment = new AliPayment();
payment.pay("赫連小伍測試支付寶支付");
payment.query("赫連小伍測試支付寶查詢");

退款裝飾器

在系統執行一段時間後,發現使用者會有支付錯誤的情況,需要退款

這時候我們就需要新增退款功能

從實際業務考慮,退款屬於整個支付功能中的一個行為。所以,應該在Payment介面類中增加退款的方法,支付寶類和微信類再分別實現這個方法

按照這個邏輯去修改程式碼,確實可以滿足退款的要求,但是需要考慮一個問題

在我們的案例中只有支付寶和微信兩個支付方式,真實場景中是有很多支付方式的。比如說華為支付、小米支付、雲閃付、中行支付、交行支付、農行支付、工行支付等等,各種各樣的支付方式加起來也有上百個

Payment介面類上增加退款方法,意味著這上百個支付方式的類都要去實現這個方法,否則程式碼就編譯報錯

工作量太大了,耗費時間太多,專案經理不可能給研發爭取這麼多的時間的

我們只能想別的辦法,比如可以這樣去實現:再定義一個類作為支付功能的擴充套件類,用來擴充套件退款功能,命名為RefundDecorator,提供一個refund()退款方法

它既然作為支付功能的擴充套件類,還應該具備支付應有的功能吧,也就是說它應該有支付查詢的方法,所以它應該實現Payment介面類

實現這個介面類就意味著要重寫pay()query()兩個方法

重寫太麻煩了,而且每個支付方式類都已經重寫過這兩個方法了,可以直接拿過來用。

我們把Payment類作為RefundDecorator的一個屬性注入就來,這樣就可以使用Payment類的pay()query()兩個方法了

在使用Payment類這個屬性之前,還要給它提供一個例項化的機會

基於以上要求,我們劃一下重點,RefundDecorator類的修改邏輯如下

  1. 實現Payment介面類,並重寫pay()query()兩個方法
  2. Payment物件作為一個屬性,並在構造器中對其進行例項化

用程式碼實現就是這個樣子

修改過後,使用者再使用支付寶支付功能時就可以這樣呼叫

RefundDecorator refundDecorator = new RefundDecorator(new AliPayment());
refundDecorator.pay("赫連小伍測試支付寶支付");
refundDecorator.query("赫連小伍測試支付寶查詢");
refundDecorator.refund("赫連小伍測試支付寶退款"); // 退款

其實,RefundDecorator類就是一個裝飾器,這種編碼方式就是裝飾器模式

文章開頭也說了,裝飾器模式是套娃的,這裡也看不出來啊

先彆著急,我們再把這個案例延申一下

對賬介面卡

我們系統又執行了一段時間,使用者既可以支付又可以退款了,使用者很滿意

可是,商家不滿意了。因為系統每天統計的商家收入和商家的實際收入有差異,比如,昨天商家實際收入1萬元,可是系統統計的只有9千元

商家對我們的系統極其不信任,要求我們必須儘快修復問題

這時候我們就需要引入對賬功能,用來保證商家的實際收入和系統統計的收入資料一致

按照我們剛才新增退款功能時的邏輯來分析

不能在Payment類中增加對賬方法,因為所有子類都需要重寫該方法,工作量太大

只能新增一個賬單類BillDecorator,裡面提供一個對賬方法check()

賬單類BillDecorator作為支付功能的擴充套件類,它應該具備支付的所有能力,包括支付查詢退款

退款擴充套件類RefundDecorator正好有這三個方法,所以讓賬單類直接繼承退款擴充套件類就可以了

但是別忘了,在退款類中需要給Payment提供例項化的機會,目的在於呼叫Payment類的pay()query()方法

所以在賬單類中也要給Payment提供例項化的機會,它才也能呼叫這兩個方法

還有一個refund()方法不能呼叫,所以還要給退款方法所在的類RefundDecorator提供例項化的機會

總結一下就是賬單類裡面要給PaymentRefundDecorator兩個類提供例項化的機會

因為RefundDecorator類中已經給Payment提供了例項化方法,所以我們只需要在賬單類裡面給RefundDecorator類提供例項化方法就可以了

ps:上面這段邏輯稍微有點繞,但是並不複雜,都是物件導向程式設計中繼承的一些基礎知識點,細讀幾遍很容易理解的

所以,賬單類的程式碼應該這樣寫

這樣我們就完成了對賬功能,其實這個賬單類也是一個裝飾器

在需要對賬的時候可以這樣呼叫

BillDecorator billDecorator = new BillDecorator(
         new RefundDecorator(
           new AliPayment()));
billDecorator.pay("赫連小伍測試支付寶支付");
billDecorator.query("赫連小伍測試支付寶查詢");
billDecorator.refund("赫連小伍測試支付寶退款");
billDecorator.check("赫連小伍測試支付寶對賬");

上面的程式碼在new物件的時候是不是像極了套娃

如果後期需求一直不斷增加,我們的程式碼可以一直套娃下去

套娃一時爽,一直套娃一直爽

總結

裝飾器模式屬於設計模式三大型別中的結構型設計模式,它的主要作用是通過將父類物件作為子類物件的屬性從而給父類物件繫結新的行為

在你希望增加原有物件的行為,而又不想要修改原有物件時,可以考慮使用裝飾器模式

裝飾器模式的實現步驟總共有兩步:

  1. 提供一個新的類定義需要擴充套件的方法,並使這個類繼承或實現原有物件,讓其擁有原有物件的所有行為
  2. 將原有物件作為新定義的類的屬性,併為其提供例項化的方法

優點

  • 無需修改原有物件即可擴充套件它的行為
  • 可以通過多個裝飾器為物件繫結不同的行為,使物件的使用更靈活

缺點

  • 裝飾器的層級遞進關係比較明顯,想要實現不受順序影響的裝飾器比較困難
  • 裝飾器層級比較深時,呼叫時套娃比較多,程式碼看起來不美觀

與介面卡模式比較

裝飾器在可以不改變物件介面的前提下實現物件功能的擴充套件,介面卡模式有時還需要修改介面

裝飾器可以遞迴實現物件行為的多層繫結,介面卡不能遞迴

裝飾器主要用來擴充套件物件的功能,介面卡主要為物件原有的功能提供更多的使用場景

還是那句話,學會設計模式不是目的,理解設計模式隱含的設計思想才能無往不利

技術需要沉澱,我們下期再見

-- 以上內容來自公眾號 赫連小伍 ,轉載請註明出處

相關文章