iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 6
0

今天的主題來到了 Decorator Pattern 修飾模式,在進入內文前,讓我們先看一下 GoF 四人幫為它下的定義。

將額外權責動態附加於物件身上,不必衍生子類別及可彈性擴增功能。

(取自 物件導向設計模式−可再利用物件導向軟體之要素

繼續往下之前,想請讀者先記得上面這段定義的幾個重點,分別是,額外動態物件不必衍生子類別彈性擴增。唔,好像整段話都是畫重點了。(毆飛)

製卡工廠的故事

想像你正經營著一間製卡工廠,為了方便工人的操作,所有的製卡器都只有一個 make() 介面,看起來像下圖。

05_decorator_01

工廠剛開始經營的時候,只主要生產三種樣式,點點卡(Dotted)、條紋卡(Striped)以及純色卡(Filled)。為此我們有三種卡片製造器,看起來像下面這張。

05_decorator_02

過了段時間,為了增加產品多樣性,我們決定多生產兩種卡片,分別是點點條紋相間卡(DottedStriped)以及點點純色卡(DottedFilled)兩種,所以我們又新增加了兩種機器。

05_decorator_03

因為想要更加的多元,因此又新增了條紋全色卡(StripedFilled)和點點條紋全色卡(DottedStripedFilled)

05_decorator_04

隨著生產越來越多樣的卡片,加入系統的製卡器也越來越多;然而,我們注意到當我們需要製造新的卡片類型的時候,需要對系統進行修改,要對原有系統增加製造新產品的功能,並且不能讓製卡器壞掉。

看起來,透過繼承(Inheritance)來新增不同的製卡器似乎有點不太好維護啊。

發生了什麼事?該怎麼辦呢?

當某類別有了一些行為需要被改變的時候,我們經常會自然地選擇繼承(Inheritance),在物件導向程式開發的過程中這是很常見的狀況。然而我們必須要記得下面兩件事。

  • 繼承是靜態的,我們不能在執行中動態地改變物件的行為,只能更換成另一種物件。
  • 在多數的物件導向語言中的繼承,一個類別只能有一種父類別。

因此,當我們在使用繼承的時候,要注意這父類別跟子類別是不是真的有行為上以及屬性上的相依。如果新增子類別的目的是擴增父類別既有的行為,但兩者基本上並沒有直接的關連,通常使用聚合(Aggregation)或組合(Composition)會是更好的選擇。

分析一下

讓我們回頭看看故事中可以觀察到的事情,在圖二和圖三中新增的製卡器在製造卡片時,與最一開始的三種製卡器並沒有絕對性的不同;更甚者,新增的製卡器更像是基於其中一種製卡器的結果,再拿去給另一種製卡器加工。

例如,點點條紋卡可以是先製造點點卡,再把這張卡拿去當作條紋卡製造機的輸入。這使得點點條紋卡製卡器像是把點點卡製卡器包裝進條紋卡製卡器在裡面。相同的,點點全色卡就像是把全色卡製卡器放進點點卡製卡器裡。可以發現,越複雜多樣的製卡器,其實只是把其他的製卡器包裝起來,並且可以透過不同的包裝順序,組合出不一樣的輸出成果。

類似這種設計,擁有同一種介面的每一個類別,都能夠被使用來去修飾(decorate)另一個類別的輸出時,就是所謂的 Decorator Pattern 修飾模式–它也有個別名是 Wrapper

來了解一下 Decorator Pattern 的經典樣式

一般而言, Decorator Pattern 的設計如下面這張類別圖,對主程式來說,它只需知道有一種元件會回應operation()方法。

05_decorator_05

而程式執行的時候,會像是下面這張圖。

05_decorator_06

來看看套用了 Decorator Pattern 的製卡器實作吧!

class Worker
  def operate(card_maker)
    card_maker.make if card_maker
  end
end

# Decorator
class Card
end

class CardMaker
  attr_reader :other_maker

  def initialize(other_maker)
    @other_maker = other_maker
  end

  def make
    if @other_maker
      @other_maker.make
    else
      Card.new
    end
  end
end

class DottedCardMaker < CardMaker
  def make
    card = super
    print_dots(card)
    card
  end

  def print_dots(card)
    ...
  end
end

class StripedCardMaker < CardMaker
  def make
    card = super
    print_strips(card)
    card
  end

  def print_strips(card)
    ...
  end
end

class FilledCardMaker < CardMaker
  def make
    card = super
    fill_color(card)
    card
  end

  def fill_color(card)
    ...
  end
end


custom_card_maker = DottedCardMaker.new(FilledCardMaker.new)

worker = Worker.new
worker.operate(custom_card_maker)

Decorator Pattern 的優缺點

優點

  • 不必新增子類別就可以延伸物件的職責
  • 可以動態地方式擴增或移除物件的職責
  • 使用組合的方式讓物件擁有不同的行為
  • 符合單一職責原則(SRP)

缺點

  • 因為使用時通常會變成 Stacked decorators,因此當需要刪除其中一個 Decorator 時會比較麻煩
  • 被包裝後的物件行為必須高度依賴於包裝的順序
  • 實例物件的程式碼不是那麼的美觀

與其他設計模式的比較

Chain of Responsibility (CoR)

讓多個物件都有機會處理某一訊息,以降低訊息發送者與接收者之間的耦合關係。它將接收者物件串連起來,讓訊息流經其中,直到被處理了為止。

(取自 物件導向設計模式−可再利用物件導向軟體之要素

因為 Decorator Pattern 也是透過遞迴的方式來完成最後的結果,因此常常被拿來跟 Chain of Responsibility 相提並論。可以注意的是,後者可以中斷整個遞迴的進行而前者不能,另一個不同點是後者的實作類別通常會有不相關且獨立的行為,而前者只是擴增既有的行為。

Strategy

定義一整族演算法,將每一個演算法封裝起來,可互換使用,更可在不影響外界的情況下個別抽換所引用的演算法。

(取自 物件導向設計模式−可再利用物件導向軟體之要素

與 Strategy 相較,Decorator Pattern 是調整了物件的外觀,而 Strategy 則是改變了物件的內在。

Composite

將物件組織成樹狀結構、「部分-全體」層級關係,讓外界以一致性的方式對待個別物件和整體物件

(取自 物件導向設計模式−可再利用物件導向軟體之要素

Composite 跟 Decorator Pattern 擁有很類似的類別圖,因為他們都是遞迴的把擁有同一種介面的類別組織起來,然而有幾個不同的地方可以區別他們。

  1. Decorator Pattern 只有一層繼承
  2. Decorator Pattern 是擴增被包裝物件的行為,而 Compositie 僅僅是把所有物件的結果加總起來。

總結

整理一下,當我們發現下面這幾點的時候,就還挺適合使用 Decorator Pattern 的!

  1. 我們需要在程式執行時,動態地增加物件的行為
  2. 新增新的行為時,不能破壞使用該物件的程式碼。
  3. 當我們沒有辦法透過也不適合透過繼承去改變類別行為時候。

參考資料

作者:Yenting


上一篇
[Design Pattern] Composite 組合模式
下一篇
[Design Pattern] Observer 觀察者模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言