Refactor | 這 3 個重構技巧我很喜歡,推薦給你 feat. JavaScript

神Q超人
Starbugs Weekly 星巴哥技術專欄
8 min readApr 28, 2023
Photo by Christopher Briscoe on Unsplash

Hi!大家好,我是神 Q 超人!其實在去年 2022 我也是看了兩三本書,但是一直瘋狂 input 的同時都沒有在 output,感覺和之前提到的 WDL 有點衝突,這可能也是我覺得在去年一直都沒什麼長進的原因 😂,雖然還是能夠將學習到的新知識使用在工作上,但是當要和同事解釋為什麼要這樣寫的時候,就會變得格外的困難。

所以為了不讓思考能力無止盡的下墜,這篇文章會分享近期在讀完 重構這本書 後,我很喜歡的幾個技巧,希望你也會喜歡!

Replace Conditional with Polymorphism

這個重構技巧的中文叫做「將條件式換成多型」。條件判斷一直是在程式裡面產生複雜度的小壞蛋,雖然只有一兩個的時候不會有什麼影響,但如果一直放任重複的判斷散落在程式碼中,那之後要再修改或增加條件時,就必須要找出所有要改的地方,少一個都不行。

以下方這個例子來說,一筆交易的狀態是否成功會影響到頁面如何呈現資訊給使用者:

在上方的程式碼案例中,雖然兩種判斷邏輯是相反的,但交易狀態是否為成功的判斷就重複寫了兩次,如果之後要再增加不同的交易狀態,像是處理中之類的,就要再檢查與狀態相關的條件判斷需不需要增加或修改。

那該如何在下一次需要修改之前,先用 Replace Conditional with Polymorphism 重構來改善呢?

其實最簡單的方法,就是依照傳進來的交易資料,統一在相同的地方產生各種狀態的交易紀錄所需要顯示在畫面上的資訊。像這樣子:

把需要顯示在頁面上的交易資訊都集中到 getTransactionInfo 裡面,而原本的兩個條件判斷也變成只有一個了,這麼一來如果要加新的狀態顯示的話,就可以專注修改 getTransactionInfo 就好,像是把它改成 switch case 之類的:

如果 getTransactionInfo 裡面塞了太多東西的話,也可以考慮使用 class 來產生對應狀態的交易物件,如果換成 class 也可以利用繼承關係,把一些共用的資訊放到父類別裡:

然後 getTransactionInfo 也可以移出 TransactionInfo component 內,並在裡面就直接使用 class:

雖然這麼一來會讓原本簡單的程式碼變的更複雜,檔案也變得更多了,但其實只是把原本在 UI 上的複雜度給移動到 getTransactionInfo,並增加了處理各種交易狀態要顯示的資訊的結構,讓 UI 變成單純顯示資料的地方。

當然這種 Replace Conditional with Polymorphism 也不只是能夠移除在 UI 上的條件判斷,如果你發現有某個東西的判斷特別頻繁,也能夠切分出不同類型上的話,這個重構方法就很適合使用,且如果相同類型的判斷越多,就越值得用,尤其是在最後看到一堆條件判斷式都消失的時候,都想直接原地下班了。

Replace Subclass with Delegate

第二個重構技巧是 Replace Subclass with Delegate,它的中文為「將子類別換成委派類別」。這個重構技巧會用在類別出現另外一組更值得做繼承的情況,或是單純想要將目前的繼承拆開,不讓兩種類別那麼緊密時使用。

以第一個例子的交易資訊來說,剛剛我們把交易資訊作為父類別,而關於交易資訊的狀態,像是「成功的交易」、「失敗的交易」、「等待中的交易」作為子類別,但如果有天我們認為「可退款的交易」和「不可退款的交易」應該更適合作為交易資訊的子類別時,就必須將原本作為子類別的交易狀態們轉換成委派處理。

好的,那該怎麼做呢?首先一樣找出取得交易資訊的地方,依上方的例子就是 getTransactionInfo 方法:

但就目前來說我們先不管它,只是要把決定交易狀態行為的邏輯,也就是上方的 switch case 移動到 Transaction 類別的 consturctor 裡面:

接下來要檢查交易狀態內的資料有哪些,我們會需要在 transaction 中透過 getter 從委派物件中取得,像是 statusString 以及 isNonRefundable,因為將來就不會用交易狀態的子類別建立物件,而是可否退款交易的子類別,所以透過把這些屬性轉換到 Transaction 的 getter 中,那使用方的程式碼就都不用修改了:

接著回到 getTransactionInfo 中,就能夠將原本產生交易狀態子類別的 switch case 移除了:

最後還要將原本在子類別的繼承關係移除,也因為交易狀態等類別的定位改變了,所以這裡順便修改了它們的命名:

重構完成後,Transaction 已經沒有任何子類別了,現在就可以開始根據新需求將是否為可退款的交易子類別建立出來囉!

另外一點就是,雖然上方的例子都沒在委派類別取得 transaction 資料後做任何事情,但如果有操作需要原本的資料的話,也是可以在委派類別中新增一個 host 屬性把送進來的 transaction 存起來哦!

Introduce Special Case

Introduce Special Case 的中文是「加入特例」。如果說 Replace Conditional with Polymorphism 是用來處理對一個物件的多種類型做重複判斷的重構,那 Introduce Special Case 就是用來處理物件的特殊情況。舉例來說,當你常常需要在多個地方判斷某個使用者是否未登入,並針對未登入的情況做對應的處理,那未登入的狀況就是使用者這個物件的特例。

假設在一個部落格的網站,未登入的使用者中必須顯示為遊客,且在瀏覽文章時不能回覆文章,必須要導到登入頁面,那在程式碼裡面我們可能就會這樣子寫:

諸如此類判斷是否有使用者的條件判斷會充斥在程式碼當中,且因為在沒有登入的情況下 user 的值會是 null,所以在使用 user 的時候就必須時刻謹惕著必須要使用 Optional chaining,如果沒有留意的話,就可能會發生從 null 中取值的錯誤。

而 Introduce Special Case 就是為了改善這種狀況的重構技巧,首先可以先觀察 user 是從哪邊統一取得的,以上方的程式碼來說,就是從 useUser 來的,以下是簡化的 useUser 內容,主要就是透過 apis.getUser 取得使用者資料,如果未登入的話 user 就會是 null:

接著我們就可以到 useUser 裡面將特例狀況導入,最簡單的方法是新增一個方法,讓那個方法可以回傳未登入使用者要顯示或做的事情。例如上方在 HeaderComment 的差異為顯示的名稱 name 和送出留言的 submitComment 事件,然後在 useUser return 時依 user 的值判斷要回傳什麼:

這麼一來在 HeaderComment 就可以移除掉那些對特例的判斷了:

看起來不只清楚多了,也不必擔心 user 有可能會是 null 的狀況,因為當它是 null 時,背後一定會有個安全的特例物件扛住!

這個 Introduce Special Case 在書中的前一個版本中被稱作 Introduce Null Object,因為就如同上方的例子,null 也是一個需要特別處理的特例之一,因此在本書中將此技巧改為更通用的 Introduce Special Case。

但除了自己另外寫之外,像是 React 或 Vue 中都可以對 Props 設定一個 default 的值,讓 Component 在沒有拿到 Props 時也有一個 default 物件可以運作,現在看來預設值好像也是一種特例處理!

總結

那最後針對這篇文章介紹的三個重構技巧做個小整理:

  1. Replace Conditional with Polymorphism:如果一個物件有各種形態,且需要常常針對這些型態做不同的顯示時就可以使用。
  2. Replace Subclass with Delegate:如果類別有更適合的繼承關係,或是想要拆開目前的繼承關係時可以使用。
  3. Introduce Special Case:如果常常需要對某個物件或它的某個屬性做判斷,那就可以另外建立一個特例物件處理那些判斷。

在這篇文章中介紹了三個重構的方式,但除了這三個之外,在文章裡的重構步驟中,也有包含一些簡單的重構技巧,像是 Replace Constructor with Factory Function 建立統一取得相同物件的來源,或是修改變數名稱的 Rename Variable,也都是超常用到的重構技巧。

也不得不說這些重構技巧讓程式碼結構性增加的同時,檔案數量也跟著暴增,不過在好幾次要修改或增加功能時都會有種「哇!還好當初決定這麼做!」的開心感,感覺自己好像預知了未來。 😂

最後大家如果對文章中的重構技巧有任何想法,或是對文章內容有任何問題,在麻煩留言告訴我了,感謝大家!

--

--