用 JavaScript 玩轉設計模式 | 如果你用了 Tree 結構,就不能錯過 Composite Pattern(組合模式)

Photo by Claudio Schwarz on Unsplash

Hi!大家好,我是神 Q 超人!最近生活實在是太忙了,因此都沒有時間好好學習被排在待學清單裡的那些酷東西,但如果再繼續耍廢下去,又會感到魔名的恐慌 😂,所以就決定來寫篇文章來強迫自己學習,繼續之前沒有讀完的設計模式!而在這篇文章中登板的是設計模式中的 Composite Pattern(組合模式)!

目的

當你的需求要求你將資料組成一個類似 tree 的結構時,即使你或是 tree 中的任一個節點都不理解自己以下的結構到底長什麼樣子,也還是能直接透過 tree 的 root 來操作或取得整體的行為或資料。

舉例來說,如果今天需要實作計算包裝及商品價格的程式,那可能會需要用來包裝的盒子和商品的 class,然後包裝和計算的過程大概像這樣子:

目前透過一個簡單的 Array.prototype.reduce() 就能取得盒子的包材費用,以及裡面的商品總價,這樣看起來一切都非常好。但通常在你認為一切都很好的時候,就會有需求產生了。

需求是要再增加袋子作為新的包材種類,而袋子會和盒子互相搭配使用,這時候該怎麼做?單純一點的做法就是先複製 Box 後,再另外建立一個名為 Bag 的 class,然後修改 totalPrice 的算法:

最後的結果完成了新的需求,但為了完成這個需求,大概複製了快要一倍的程式碼,然後還在 totalPrice 裡面多寫下判斷是否為包材以改變計算的方式。要再做更多的話,接下來也許會建立一個 Pack 的父類別,然後透過繼承把 Box 和 Bag 內的相同邏輯放過去,雖然這麼做讓程式碼去除了重複的部分,但因為當前的實作邏輯與包材的 class 耦合,所以如果未來有新的包材,還是需要進到 totalPrice 中修改實作(加入新包材的判斷)。

那有辦法在不修改實作的狀態下增加不同的包材嗎?如果你也面臨到這個瞬間,不妨試試看 composite pattern 吧!

Composite Pattern(組合模式)

在上方的例子裡面,之所以需要另外判斷目前是否為包材,是因為在程式的邏輯中,商品和包材為不同的東西,不同的東西計算的邏輯就不一樣,這聽起來很合理,但在 composite pattern 下,我們需要把它們都當作相同的東西來取得價格。

簡單來說,就是因為包材是用 totalPrice 計算包材和被包裝的商品價格,而商品是用 price 來取得價格,所以在上方的例子中,才需要另外判斷是否為包材以執行 price 或 totalPrice,假如在 composite pattern 裡要將它們看作同個東西,那取得價格的方式就應該要一樣。

所以第一步就先將 Product 裡面的 price,改成使用與包材相同的 totalPrice 來取得價格:

因為 Product 目前取得價格的方式已經和 Box 與 Bag 相同,所以在 Box 與 Bag 內的 totalPrice 實作,就不必再判斷物件是否為包材了,只是要將實作中取得價格的地方修改成用 totalPrice:

改完後來看一下現在程式碼的全貌,以及執行的結果:

在加入 composite pattern 的過程中,僅僅是修改 Product 的使用介面,就能使計算的邏輯變得更簡單易懂,因為在計算價格的時候,不論是遇到 Box、Bag、Product 或是未來有可能加入的不知道什麼包材,雖然內部計算的實作可能不同,但只要在價格的處理上將它們都視為同種東西,統一從 totalPrice 取價格就不會有問題。

其他案例

除了上述的包材外,還有像是前幾年參加的 The F2E 2nd 中就有個題目是雲端硬碟,在該 UI 介面中,就會有許多需要顯示當前檔案路徑,或是計算檔案數量的方法。諸如此類的 tree 資料結構,就很適合使用 composite pattern 來思考各個結構內物件的實作介面,只要用的正確就能夠簡化實作的邏輯,也不必擔心實作的邏輯與物件類型之間出現耦合。

在找相關案例的時候,也有看到不只是需要「得到結果」才能用 composite pattern,在 JS Design Pattern Day08-組合模式 Composite 這篇文章裡面,則是使用在一群需要被執行的巨集,使用者可以透過該巨集執行巨集下的所有指令。這裡主要是想提醒說,雖然做什麼事不重要,但使用樹狀結構處理的資料是考慮 composite pattern 的首要條件,反過來說就是 composite pattern 會在樹狀結構下發揮價值。

這篇介紹的 composite pattern 是我自己也還沒有在實務上使用過的 pattern,唯一有印象的就是文章中提到,用來練習當作品的雲端硬碟,雖然當時實做的邏輯一點也不複雜,但似乎是若隱若現地看見 composite pattern 的影子,然後過了兩年後的現在才知道原來這也是 pattern 之一,不曉得大家看完文章後會不會也想起了某段程式碼呢?有的話就留言和我分享吧!

最後如果文章裡有任何問題或是需要補充的地方,再麻煩留言告訴我,我會盡快回覆和修改文章內容的,非常感謝! 🙌

--

--