利用 Decorator Pattern - Design Patterns 學習設計模式 Decorator Pattern,並利用 Python 撰寫 sample code.

摘要

Decorator Pattern 透過修改已定義的行為,以擴展或變更其功能,而不需透過繼承和覆寫。

使用組合 (composition) 替代繼承 (inherit),可動態地添加或移除行為,且不需要在繼承關係中堆疊子類別。

Decorator模式更具靈活性和可維護性,因此被廣泛地應用於軟體開發領域中。

繼承 (inherit) 架構面對的問題

假設當前的議題是 : 要在不同的渠道,如: Google Ads、 Meta Ads 或 Instagram,投放廣告 , 需要建立特定的資料結構來處理廣告文案與投放渠道,並採用繼承的方式來建置資料模型,資料模型間的關係初步設計可能會如下圖所示

建立一個 AdContent 並定義廣告內容的行為,為文字廣告和圖片廣告分別建立 TextAd 和 ImageAd 並讓兩者繼承 AdContent;

同時,每個投放渠道對於廣告文案的限制可能有所不同,利用繼承建立不同渠道的類別,如: GoogleTextAd 、 LineTextAd、MetaImageAd…等等,透過覆寫來設定不同渠道的限制。

基於上述設計的思考邏輯: 若後續迭代需求要求為 ImageAd 增加一個 split 的屬性,使得圖片廣告可用分割區塊的方式放入更多圖片,對已存在的 GoogleImageAd 、 MetaImageAd 和 InstagramImageAd 實現相關屬性的操作方式。

顯而易見,AdContent 理應為 content 定一個泛用性較高的資料模型。然而,若當前的資料模型無法滿足新需求,則資料模型的調整也會影響整個繼承鏈上的類別。

根本的問題在於採用繼承鏈的架構,當新需求只能適用於特定或部份渠道,卻要對繼承鏈上所有的類別進行調整;當繼承鏈過於龐大或負責的時候,會導致開發迭代版本需要更多的時間成本。

用 Composition 替代 Inherit

Decorator Pattern 則透過包裹 (wrapper) 方式來達成組合目的。

因為 AdContent 提供對文案的操作行為,而投放渠道或是廣告類型 ,如:文字或圖片,對文案而言都是附加描述,因此可以將 AdContent 定義為最基礎的元件 (component) ;

附加描述可以對元件的行為 (behavior) 或執行結果 (result),做進一步的拓展、擴充或修改,因此將附加描述定義為 decorator 。 因 decorator 是獨立的,因此也可以被其他的 decorator 包覆。

若 decorator 修改執行 behavior 的參數,或是修改了 behavior 的執行結果,則會依據 decorator 的包覆順序做出對應的改變 :

  • 若有多個 decorator 對同一個 behavior 的傳入參數進行修改,則傳入參數的改變會從 outer decorator 影響 inner decorator ,最終影響 component 執性 behavior 時的傳入參數

  • 若有多個 decorator 對同一個 behavior 的執行結果進行修改,則從 component 回傳的執行結果開始,由 inner decorator 的修改影響 outer decorator 的修改,直至最外層的 outer decorator 回覆執行結果。

設計方法

依據先前的小節說明整理出幾點資訊

  • 需要定義出 元件 (component)附加描述 (Decorator)
  • 執行 behavior 是由外層 decorate 到內層 component , behavior 的 signature 需統一,可判斷附加描述是元件的一種 (is-a)
  • 附加描述可以疊加、 outer decorator 需要呼叫 inner decorator ,可判斷附加描述應具備 inner decorator 或 component (has-a)

再依據上述三點描述,以廣告文案投放至不同渠道的議題為例,用 Decorator Pattern 方式設計類別之間的關係,如下圖所示

令 Decorator 繼承 Component 類,並在 Decorator 的建構方法傳入 component 實體 ( object | instance ) , 使 decorator 具備包裹 (wrapper) 其他 decorator 和 component 的能力。

Demo

用 Python 撰寫 Decorator Pattern 的示例。
Source code

以下分別提供了 4 種示例

示例 1

Single decorator wrapped component

print(f'Example 1 - text ad decorator output:')
print(TextAdsDecorator(text_ad).get_content())
print(f'Example 1 - image ad decorator output:')
print(ImageAdsDecorator(image_ad).get_content())

示例 2

Apply multiple decorators

print(f'Example 2 - Google Ads with text ad decorator output:')
print(AdsChannelDecorator(TextAdsDecorator(text_ad), channel="Google Ads").get_ad_channel())
print(f'Example 2 - Meta Ads with text ad decorator output:')
print(AdsChannelDecorator(TextAdsDecorator(text_ad), channel="Meta Ads").get_ad_channel())

示例 3

Apply multiple decorators with different order

print(f'Example 3 - image ads decorator output:')
print(AdsChannelDecorator(ImageAdsDecorator(image_ad), channel="Youtube Ads").get_description())
print(f'Example 3 - exchange decorators output:')
print(ImageAdsDecorator(AdsChannelDecorator(image_ad, channel="Instagram Ads")).get_description())

示例 4

Inherit decorator class to setting different behavior

print(f'Example 4 - inherit image ads decorator output:')
google_image_ads = GoogleImageAdDecorator(AdsChannelDecorator(image_ad, channel='Google Ads'))
# assume google image ads size limitation with: 320 <= width <= 1920, 480 <= height <= 1080
google_image_ads.width = 2560.0
google_image_ads.height = 1440.0
print(f'Over Google Image Ads size limitation')
print(google_image_ads.get_description())