iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 3
0
自我挑戰組

來讀設計模式:Junior developer 跟大家一起練功系列 第 16

DAY16: Decorator 模式

本篇將介紹到

  • 何謂 Decorator 模式
  • 回到電子商務案例去看如何使用它
  • 它的關鍵特徵
  • 使用 Decorator 模式該注意到的事

我們開始吧!

何謂 Decorator 模式

GoF 說道:

(Decorator 模式)動態地給一個物件一些額外的職責。就增加功能來說,decorator 模式比生成子類別更為靈活。

工作原理

它的工作原理:

  • 始於一個 Decorator 物件(負責新功能的物件)
  • 生成一條物件「鏈」
  • 終於原物件

先來看看 UML 圖

這次我們先來看看 decorator 策略的 UML 圖

本圖隱含了上述的物件鏈:

  • 每條鏈都始於一個 Component 物件(ConcreteComponentDecorator
  • 每個 Decorator 物件後面都接著另一個 Decorator 或是 ConcreteComponent
  • 物件鏈都會終於一個 ConcreteComponent 物件

Interesting, right?

我相信到這裡可能還是有點模糊,不知該怎麼實踐它。我們接下來就看看案例。

回到電子商務案例去看如何使用它

本例是 Day10 提到的電子商務系統,就不在此贅述。

新需求:在 SalesTicket 增加 Header 和 Footer

我們當時有說到,當 SalesOrder 計算完商品金額後,它會使用 SalesTicket 幫忙列印票券,如下圖。

今天有了新需求:我們需要為 SalesTicket 物件新增表頭 (header) 與頁腳 (footer)。

簡單的實踐辦法會是:新增 HeaderFooter 物件,讓 SalesTicket 使用該兩個物件, 配上 switch-case 做判斷,來將 header 與 footer 加上去。

但是如果今天的判斷變複雜了:如果今天有多種 headers 與 footers 呢?如果有機會會需要同時有兩種 headers 在同一張票券呢?

多種組合的可能,可能使上述簡單的結構無法承擔。

這時候如果我們引入 decorator 模式,我們就會有下面的圖:

此處的 Client 即是 SalesOrder。我們接下來來看實踐的程式碼如何。

實踐 decorator 模式的程式碼

Client 做的事:

class Client {
  	public static void main(String[] args) {
      	Factory myFactory;
      	myFactory = new Factory();
      	Component myComponent = myFactory.getComponent();
    }
}

Component 抽象類別與其一的衍生類別:SalesTicket(上述的 ConcreteComponent):

abstract class Component {
  	abstract public void printTicket();
}
class SalesTicket extends Component {
  	public void printTicket() {
      	// 列印銷售票券的程式碼
    }
}

各種 Decorator

abstract class TicketDecorator extends Component {
  	private Component myTrailer;
  	public TicketDecorator(Component myComponent) {
      	myTrailer = myComponent;
    }
  	public void callTrailer() {
      	if (myTrailer != null) myTrailer.printTicker();
    }
  
  	// 各種具體 Decorator 類別
  	public class Header1 extends TicketDecorator {
      	public Header1(Component myComponent) {
          	super(myComponent);
        }
      	public void printTicket() {
          	// 列印 Header1 的程式碼
          	super.callTrailer();
        }
    }
  
  	public class Header2 extends TicketDecorator {
      	public Header2(Component myComponent) {
          	super(myComponent);
        }
      	public void printTicket() {
          	// 列印 Header2 的程式碼
          	super.callTrailer();
        }
    }
  
  	public class Footer1 extends TicketDecorator {
      	public Footer1(Component myComponent) {
          	super(myComponent);
        }
      	public void printTicket() {
          	super.callTrailer();
          	// 列印 Footer1 的程式碼
        }
    }
  
  	public class Footer2 extends TicketDecorator {
      	public Footer2(Component myComponent) {
          	super(myComponent);
        }
      	public void printTicket() {
          	super.callTrailer();
          	// 列印 Footer2 的程式碼
        }
    }
}

產生物件鏈的工廠物件

class Factory {
  	public Component getComponent() {
      	Component myComponent;
      	myComponent = new SalesTicket();
      	myComponent = new Footer1(myComponent);
    }
}

觀察最後這個 Factory,我們可以知道 myFacory.getComponent() 會使這張 ticket 印出來的樣式為:

HEADER1
SALES TICKET
FOOTER1

如果今天我們需要的是

HEADER1
HEADER2
SALES TICKET
FOOTER1

那麼 myFactory.getComponent() 所需要的物件鏈就會是

return new Header1(new Header2(new Footer1(new SalesTicket())));

以此類推。

它的關鍵特徵

以下是 decorator 模式的關鍵特徵:

項目 內容
意圖 動態地給一個物件增加職責
問題 要使用的物件將執行所需的基礎功能。但可能需要為這個物件增加某些功能,附加功能可能在基礎功能之前或之後。
解決方案 可以無須建立子類別,而擴展一個物件的功能
參與者與協作者 ConcreteComponentDecorator 物件為自己增加功能。有時候也可能用 ConcreteComponent 的衍生類別提供核心功能。Component 類別定義了所有上述類別所使用的介面。
效果 增加的功能放進小物件中。這樣可以動態地在 ConcreteComponent 物件之前或之後新增功能。注意到物件鏈必終於 ConcreteComponet
實作 建立一個抽象類別來表示原類別和要增加到這個類別的新功能。在裝飾類別中,將對新功能的呼叫放在緊隨其後的物件的呼叫之前或之後,以獲得正確的順序。

使用 Decorator 模式該注意到的事

幾個約束因素

我們要注意到這個模式有幾個約束因素:

  • 存在幾種可選的功能
  • Decorator 可遵循也可不遵循所有規則
  • 需要某種方式以所需的不同順序呼叫 Decorator 物件們,但又不能增加客戶負擔(使客戶增加某些職責)
  • 不希望應用程式必須承擔知道使用哪些 Decorator 物件的職責。

試著 follow 上述的約束,可以讓 decorator 模式的意圖與實作分離開來。

接下來

接下來,我們將會介紹 Observer 模式。明天見囉!


上一篇
DAY15: 使用設計模式的原則與策略
下一篇
DAY17: Observer 模式
系列文
來讀設計模式:Junior developer 跟大家一起練功22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言