iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0

https://ithelp.ithome.com.tw/upload/images/20240928/20168201nQwIC0pSa8.png

今天要介紹的是 Command 模式,這是 GoF 定義的行為型模式之一。

情境

開發複雜應用程式時,開發者會需要一個靈活的架構來處理各種操作或功能,這些操作可能來自不同模組或元件。操作某功能的觸發來源可能有很多種,例如按鈕、語音命令、鍵盤輸入等,在這些狀況下,操作的邏輯通常會直接由觸發來源控制。以音樂播放器應用程式為例,使用者可能會透過按鈕、語音命令或鍵盤輸入來控制音樂的播放、暫停或音量調整等功能。

當使用者執行某操作時,開發者希望能封裝該功能,以便更容易地管理、儲存或重用這些操作,同時也希望保持操作間的靈活性,使不同操作間互不干擾。此外,開發者還希望能輕鬆的修改操作的具體實現,例如修改操作的名稱或參數等,而不需要對系統的其他部分進行大量修改。

問題

如何設計一個系統,使不同操作能獨立於其觸發來源進行處理,同時能在必要時修改操作的具體實現?此外,如何確保這些操作能被有效封裝以便管理或重用,並且在不干擾系統其他部分的情況下修改?

權衡

  • 可重用性:開發者希望操作能被封裝和重用,以便在不同模組間共同使用,這代表操作要有一致的外部接口,並且要妥善封裝,但也不能過度封裝,仍要保持足夠彈性來應對未來的擴展或修改
  • 彈性:系統要能彈性的處理不同操作,並且要能輕鬆增加、修改或替換操作,這可能要解耦實際操作邏輯與觸發來源的邏輯,但解耦兩者也會增加系統複雜性
  • 穩定性:系統在允許操作能彈性變更的同時,要能保持穩定性,避免修改一個操作時影響到系統的其他部分,需要在彈性與穩定性間找到平衡
  • 複雜度:開發者需設計一種簡單、好理解的操作流程,但為了具有彈性和可重用性,可能需要引入額外的設計模式或抽象層次,而過度抽象化或過度設計可能會增加系統複雜度,導致維護較困難

在實際設計過程中,開發者需根據具體的應用場景來權衡這些因素,確保系統能既有彈性又穩定,同時保持相對簡單的結構。

解決方案

Command 模式的核心理念是「提供一種方法,將命令的發出與執行分開處理」,也就是由不同的對象負責發出命令與執行命令的邏輯。為了實現這點,我們可以將方法的呼叫、請求或運算封裝到一個單一物件中,此物件能接收各種參數,以彈性的執行對應操作。

先來看一個沒有 Command 模式的範例。假設現在有個電商的應用程式,需要一個購物車管理系統來管理購物車的相關操作,使用者可加入購物車、移除購物車商品以及查看購物車,並且這些操作會在應用程式的不同地方被觸發,例如在商品詳細頁或商品分類頁都可加入購物車,在購物車詳細頁可以查看購物車內容或刪除商品。
我們定義一個 cartManager 物件來包含這些操作:

const cartManager = {
  cart: [],

  addItemToCart(item, id) {
    this.cart.push({ id, item });
    return `You have successfully added ${item} (${id}) to your cart.`;
  },

  removeItemFromCart(id) {
    const initialLength = this.cart.length;
    this.cart = this.cart.filter(item => item.id !== id);
    return this.cart.length < initialLength
      ? `Item (${id}) has been removed from your cart.`
      : `Item with ID ${id} not found in your cart.`;
  },

  viewCart() {
    return this.cart.length === 0
      ? "Your cart is empty."
      : `Your cart contains: ${this.cart.map(item => item.item).join(", ")}.`;
  }
};

可這樣使用:

console.log(cartManager.addItemToCart("Laptop", "001")); // 加入購物車
console.log(cartManager.addItemToCart("Mouse", "002"));  // 加入購物車
console.log(cartManager.viewCart());                     // 查看購物車內容

console.log(cartManager.removeItemFromCart("002")); // 移除購物車商品
console.log(cartManager.viewCart());  // 查看購物車內容

目前的購物車管理系統可正常運作👌,但有一個潛在的問題:這種寫法緊密耦合了 cartManager 的方法與應用程式的其他部分。何謂緊密耦合? 意思是,如果 cartManager 中的方法名稱或參數有改變,則應用程式中所有直接呼叫這些方法的程式碼都需要更新。舉例來說,如果我們將 viewCart 方法重命名為 getCartInfo,那專案中所有使用 cartManager.viewCart 的地方都需要修改為 cartManager.getCartInfo,否則就會出錯。

這種緊密耦合違背了「盡可能鬆散耦合」的設計原則,導致程式碼維護較困難。因此我們需要一個更好的方式來解耦他們,而這就是 Command 模式派上用場的地方~
接下來就來看看如何以 Command 模式來優化。
我們希望可以在 cartManager 物件上執行任何命名的方法,並傳遞任何可能使用的資料/參數,預期的呼叫方式:

cartManager.execute('addItemToCart', 'Laptop', '001');

execute 實作方式如下:

cartManager.execute = function(name) {
    return (
        cartManager[name] &&
        cartManager[name].apply(cartManager, [].slice.call(arguments, 1))
    );
};

這樣我們就可以改用 execute 的方式呼叫購物車管理系統,並根據需要傳入想要的方法名稱參數。

console.log(cartManager.execute('addItemToCart', 'Laptop', '001')); // 加入購物車
console.log(cartManager.execute('addItemToCart', 'Mouse', '002'));  // 加入購物車
console.log(cartManager.execute('viewCart'));                       // 查看購物車內容

console.log(cartManager.execute('removeItemFromCart', '002'));      // 移除購物車商品
console.log(cartManager.execute('viewCart'));                       // 查看購物車內容

應用案例

最近偶然發現一個 Web API 叫 execCommand(),發現它的運作方式其實與上面提到的 Command 解決方式非常類似,我們可以將想要執行的操作名稱傳入 execCommand(),瀏覽器就會自動執行相應的操作。例如,如果要將一段文字複製到剪貼簿,可以這樣使用:

document.execCommand('copy');

不過目前這個 API 已被標記為 Deprecated,不建議大家再繼續使用這個方法,但還是提出來介紹給大家,讓大家了解 Command 模式的實際應用案例。
如果想了解更多關於 execCommand() 的資訊,可以參考官方文件

優點

以 Command 作為解決方案優點如下:

  • 可擴展性:系統能更容易擴展。如果需要增加新的操作,只需建立該操作對應的函式並在呼叫命令(操作)時傳入不同參數即可,不必大幅修改現有程式碼。此外,若需要修改功能的內部實作或方法名稱,這些更改也會相對容易進行,不會對整個應用程式造成大範圍的影響
  • 將命令與執行操作的物件解耦,適合需要在特定時間排隊進行的命令

缺點

以 Command 作為解決方案缺點如下:

  • 系統複雜度增加:在處理簡單應用或操作時,使用 Command 模式可能會過度抽象,導致系統變得過於繁瑣,增加維護困難度。如果一開始系統不需要這種複雜度,可以在有需求、遇到困難時,再考慮重構和優化設計

Reference


上一篇
[Day 13] Mediator 模式
下一篇
[Day 15] Proxy 模式
系列文
30天的 JavaScript 設計模式之旅23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言