iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0

https://ithelp.ithome.com.tw/upload/images/20240923/201682013gUG8T7lzw.png

今天要介紹的是 Decorator 模式,這也是 GoF 提出的模式之一。

情境

在軟體開發時,有時會遇到一個類別需要在執行階段時增加額外的功能,但同時又希望讓原類別盡量少被修改,以便於維護和擴展。

問題

當系統功能隨時間增加而逐漸複雜化時,直接修改原有類別結構與功能會讓系統難以理解與維護,且直接修改現有類別也可能影響原有功能,而讓原先可正常運作的功能出現非預期錯誤,或者讓單一類別承擔過多責任,該如何在不影響原有類別功能、不修改底層程式碼的情況下,動態增加額外功能?

權衡

  • 功能擴展與架構穩定性:隨著時間發展,系統會需要逐漸增加新功能,開發者要在不影響現有系統運作的前提下擴展系統功能,這代表系統架構要能支援動態、靈活的擴展,而這些擴展不能影響現有系統的穩定性或增加太多系統複雜度,因為系統複雜度與穩定性會影響系統長期維護難度和可靠性
  • 效能考量與動態擴展:在執行階段增加功能時,可能會消耗額外的計算成本和記憶體空間,進而影響系統效能,在需要高效能的應用程式中,這點考量十分重要,但同時,為了產品發展與因應市場變化,系統需要快速適應新功能需求,而這可能會在執行階段時增加或調整功能
  • 程式碼可維護性與功能彈性:為了讓系統易於維護和更新,程式碼應具備可讀性且結構清晰,過於複雜的修改和擴展可能會導致程式碼難以理解和維護,但同時又有新的功能需求,開發者需要在不重寫現有程式碼的情況下新增功能,同時維持可讀性

解決方案

Decorator (裝飾器)模式是一種促進程式碼重用的模式,裝飾器可以向系統現有類別動態添加功能,會稱為「裝飾(decoration)」是因為它帶來的功能本身不是類別必備的基本功能,而是在需要該功能時動態加入。
在前端應用中,很常需要使用按鈕元件,但按鈕又需要有不同類型和樣式,此時我們可先建立一個按鈕的基本元件:

class Button {
  constructor() {
    // 傳入預設的文字和顏色
    this.text = "Click me!";
    this.color = "red";
  }

  render() {
    return `<button style="color: ${this.color}">${this.text}</button>`;
  }

  getStyle() {
    return this.color;
  }

  getText() {
    return this.text;
  }
}

接著我們需要建立多個裝飾器來裝飾按鈕物件,在建立裝飾器之前我們先建立裝飾器的基本 class,之後的裝飾器都會 extends 這個裝飾器基本 class,避免直接 extends Button 這個 class,基礎裝飾器 class 可以為多個裝飾器提供共通接口和基本行為,所有裝飾器都可繼承它,能確保裝飾器間的一致性,也避免直接改到 Button 的基本行為,以下為基本裝飾器類別:

class ButtonDecorator extends Button {
  constructor(button) {
    super();
    this.button = button;
  }

  render() {
    return this.button.render();
  }
}

再來就可因應需求來建立多個裝飾器:

// 為按鈕增加 icon 的裝飾器
class IconButtonDecorator extends ButtonDecorator {
  constructor(button, icon) {
    super(button);
    this.icon = icon;
  }

  render() {
    const originalRender = this.button.render(); // 先得到基本按鈕的 render 結果
    // 用字串 replace 的方式改變基本的 render 結果
    return originalRender.replace(/<\/button>$/, ` <span>${this.icon}</span></button>`);
  }
}

// 為按鈕改變顏色的裝飾器
class ColorButtonDecorator extends ButtonDecorator {
  constructor(button, color) {
    super(button);
    this.newColor = color;
  }

  render() {
    const originalRender = this.button.render();
    // 更新字串中的 style屬性
    return originalRender.replace(/style="[^"]*"/, `style="color: ${this.newColor}"`);
  }
}

在基本情境下,我們可使用 Button 元件即可,但如果需要 icon 或要改變顏色,就可透過裝飾器來疊加改變按鈕的屬性,同時維持基本 Button class 不變,以下為使用範例:

// 首先先初始化按鈕
let myButton = new Button();

// 添加裝飾
myButton = new IconButtonDecorator(myButton, '🌟');
myButton = new ColorButtonDecorator(myButton, 'blue');

console.log(myButton.render());
// 會印出 <button style="color: blue">Click me! <span>🌟</span></button>

優點

  • 促進程式碼重用:我們透過裝飾器將功能分離,每個裝飾器負責各自的特定功能(單一職責),讓程式碼能更容易的被重用
  • 提高靈活性:可在程式碼執行時動態的增加或刪除裝飾功能,以應對應用程式多樣性的需求,同時不必擔心基底物件被修改,也不需依賴大量子類別,各裝飾器可獨立運作
  • 維持物件封裝:如上所述,裝飾器可獨立於原始物件來增加功能,可維持原本基底物件的封裝性和獨立性,保持原始物件不被修改

缺點

  • 增加複雜度:裝飾器模式可能會引入過多小型又相似的物件(如上範例,產生好多 Button...),如果缺乏妥善管理,會讓應用程式的架構變複雜
  • 難以追蹤裝飾:當裝飾鏈過長,疊加太多裝飾時,要追蹤一個物件被哪些裝飾器裝飾過會變得困難
  • 不熟悉此模式的開發者可能較難理解使用它的原因,而讓裝飾器變得難以管理,但足夠的註解和模式研究可改善此問題
  • 效能考量:在執行階段動態增加裝飾可能會影響效能,尤其在裝飾鏈很長的情況下,會讓運算變得複雜

其他補充

這裡舉例的 Decorator 模式比較簡單,網路上可以看到很多複雜的應用範例,但我想說以簡單範例來了解此模式的核心理念就好,如果有興趣的讀者可以再看看 Reference 的文章~

Reference


上一篇
[Day 08] Facade 模式
下一篇
[Day 10] Flyweight 模式
系列文
30天的 JavaScript 設計模式之旅12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言