重構-改善既有的程式設計feat.實戰經驗

Joe Chang
Coding Hot Pot
Published in
8 min readOct 7, 2023

--

photo by https://unsplash.com/@sepoys

這本書在我六年前剛轉職的時候有看過一遍,但那時的我因為經驗不足,其實看完沒有什麼感觸,現在累積了幾年的工作經驗再來二刷這本書,就蠻有共鳴了,開始可以體會何謂程式碼的「壞味道」,因此隨手將書中我覺得不錯的內容摘要下來,並且會結合我先前重構的經驗跟大家分享

重構的最終目的就是降低bug發生的機率,減少改a壞b的可能性,讓大家可以輕鬆地維護這包程式碼

當你發現你的函式做了太多事情的時候,超過六行的程式碼就會開始有壞味道,是該考慮將部份的功能抽離出來了,這樣一來也會比較好維護和除錯,

👉 我真的看過一個函式快要五百行,每次光是找對應的功能就找超久…

====重構前=====
function buy(){
//計算總價

//套用折扣碼

//一大堆...

}

====重構後====

function calcTotal(){
//計算總價
}

function handleCupon(){
//套用折扣碼
}

function buy(){
let amount = calcTotal()
handleCupon()
//略...
}

函式和變數的命名要明確,讓別人可以透過名稱就知道這個函式的目的是什麼,假設有在用ai 輔助工具(ex. Copilot)寫程式的人,好的命名可以讓ai推論出更符合你要的需求的程式碼

灰色文字為ai的推論結果

👉 a、b、 x、 y、 z這類的命名真的是先不要,絕對會讓你的同事抓狂

避免使用全域變數,會很難知道有哪些程式碼接觸它

👉 早期還沒開始用前端框架開發時,真的就是用了一堆全域變數,要追查是哪些地方有修改到這個變數非常非常花時間

將函式參數化,如果兩個函式有非常相似的邏輯,使用不同的常數,可以將 它們改成一個函式,透過傳入不同的參數來做到差異化,增加函式的實用性

====重構前=====
function tenPercentRaise(aPerson){
aPerson.salary = aPerson.salary.mutiply(1.1);
}

function fivePercentRaise(aPerson){
aPerson.salary = aPerson.salary.mutiply(1.05);
}

====重構後====
function raise(aPerson, factor){
aPerson.salary = aPerson.salary.mutiply(1 + factor);
}

👉 早期寫程式的時候都不會發現自己寫了很多相似的函式 😂

移除旗標引數,因為它們會讓我難以理解函式有哪些呼叫方式

====重構前=====
function setDimension(name, value){
if(name === 'height'){
this._height = value;
return;
}
if(name === 'width'){
this._width = value;
return;
}
}

====重構後====
function setHeight(value) { this._height = value }
function setWidth(value) { this._width = value }

👉 透過參數(name)來識別value要賦值給哪一個變數,但這樣寫反而會增加一堆條件判斷式

將參數換成查詢程式,讓參數可以保持簡單,降低呼叫方的心智負擔

====重構前=====
availableVacation(anEmployee, anEmployee.grade, anEmployee.salary);

function availableVacation(anEmployee, grade, salary){
//計算獎金
}

====重構後====
availableVacation(anEmployee);

function availableVacation(anEmployee){
const { grade, salary } = anEmployee
//計算獎金
}

👉 假設傳入function的參數都是某個物件的屬性的話,就直接傳入物件即可

遇到難以閱讀的運算式就提取變數吧!

====重構前=====
return order.quantity * order.itemPrice -
Math. max (0, order.quantity - 500) * order.itemPrice * 0.05 +
Math. min(order.quantity * order.itemPrice * 0.1, 100);

====重構後====
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math. max (0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;

👉 為了提高程式碼可讀性,複雜的計算或是條件判斷都可以抽成變數

盡可能地使用early return

====重構前=====

function getPayAmount () {
let result;
if (isDead)
result = deadAmount ();
else {
if (isSeparated)
result = separatedAmount () ;
else {
if (isRetired)
result = retiredAmount ();
else
result = normalPayAmount () ;
}
}
return result;


====重構後====

function getPayAmount() {
if(isDead) return deadAmount();
if(isSpearated) return separatedAmount();
if(isRetired) return retiredAmount();
return normalPayAmount();
}

👉 以前很容易就寫出巢狀的條件判斷式(波動拳等級),使用early return可以降低程式複雜度

重構最害怕的就是將原有的功能改壞,所以一定要有完整的自動化測試support,才能無後顧之憂

👉 之前的重構給的工時非常短,因此測試什麼就放生了,完全沒寫單元測試,只靠RD簡單手動自測,果不其然重構之後衍生了不少的bug,就變成上線之後讓客戶來幫你測試😉

將重構的步驟拆解成很多小步驟,在除錯上會更容易

👉 重構就像拆彈作業一樣,小心謹慎一步一步來,確保程式碼可以正常運作

程式碼好壞的關鍵,在於它有多麼容易修改,如果有人想要修改,他們可以輕鬆地找到需要修改的地方

👉 我想大多數的人都會認同這句話,好的程式碼不需要花太多時間trace code,就能夠找到要改的地方

不良的程式設計常用更多程式碼做同一件事,因此重構有一個很重要的面相是:消除重複的程式碼,才不會發生我明明修改了A處程式碼,卻沒有如我預期的運作,就是因為我沒有改到B處地方的程式碼

👉 之前曾經有過一個經驗,要調整某個功能的計算公式,但是我發現明明已經改了A地方的函式卻沒有套用新的公式,排查後才發現還有B、C、D函式等漏網之魚,如果可以抽成共用函式的話,維護上會輕鬆很多

移除沒有用到的程式碼

👉在重構的時候很常發現有些函式都沒有地方呼叫,隨著需求的變更,或許這個功能已經不需要了,那麼就毫不留念的拔掉它吧!

重構的挑戰

我個人認為重構向來是一項吃力不討好的工作,當你說要重構的時候,主管或是pm可能會問你,好端端的為什麼要重構?對他們而言,現有的架構都能夠正常運作,何必沒事找事做呢?這樣豈不是會影響新功能的開發時程嗎?有的人甚至認為重構根本沒有任何的產出,因此該如何說服對方,讓他們了解重構的好處與價值,是一項非常難的課題

結語

書中提到不少的重構手法,有趣的是有些重構手法是相互牴觸的,有點像矛盾大對決那種感覺,沒有所謂的某某作法一定是最好的,因此在重構的時候還是要看自身需求決定採取哪種手法,這本書還列舉非常多物件導向的優化例子,有機會的話強烈大家可以看看這本書!

--

--

Joe Chang
Coding Hot Pot

前端工程師,唯有非常努力,才能看起來毫不費力