上次已經巡完目前的程式碼,聞到一些具有「異味」的程式部分,我將其稱為「程式異味點」。意味著這些部分是邏輯相對複雜,不容易理解,有值得重構優化的部分。
再次強調一個概念,程式碼壞味道是個「指標」,凸顯出這裡有可以優化的空間,至於重構的方法,則是因程式而定和異味而定。(可以參考「重構:改善既有程式」中的附錄,裡面有異味 & 重構方法的對照表)
接下來進入正題,動口不如動手,捲起袖子下去進行重構囉。
上次已經聞到不少怪味道的程式碼,手開始有點癢了,就開始來…列一下問題點(Code Smells)的 Todo 清單,誰叫我是 Todo 魔人(?
handleInput
部分沒有處理阻擋不能輸入的部分Keypad
部分的按鍵處理部分,if / else 巢狀結構過深handleEdit
和 handleOk
這些 function 內的邏輯,有些地方重複App
共有 7 個狀態,有點太多,不好理解大致上是以上這些地方有異味,問題點大致上是 4 個,彼此沒有重複涵蓋邏輯。
首先是 handleInput 的部分,先請 AI 幫我處理看看,總之就是要不能出現像是如下「非正常可輸入金額」
這與其說是「重構」,不如說是功能的一部分,這晚點再來處理。還記得上次說的嗎?
重構不能加上新功能邏輯
「使用者只能輸入特地規則的數字」,原本沒有做此規則,使用者可以隨意輸入,確實算新功能,所以先 hold 著。
下面一位~
Keypad
部分的按鍵處理部分,if / else 巢狀結構過深<button
key={index}
className={`p-4 text-xl font-bold rounded ${
btn === "AC"
? "bg-orange-500 text-white"
: btn === "⌫"
? "bg-yellow-500 text-white"
: btn === "+"
? "bg-gray-600 text-white"
: "bg-gray-700 text-white"
}`}
onClick={() => {
if (btn === "AC") onClear();
else if (btn === "⌫") onBackspace();
else onInput(btn);
}}
>
{btn}
</button>
腦中是有一些解法了(用多型來處理),但先問問 AI 看看,它有什麼好想法?用了 inline 指令問了 claude-sonnet 看看:
以下的 <button ... > 有 Code Smell,可以幫我試著重構嗎?
雖然 AI 給了我解法,幫我用 switch 做了重構,是解決掉 if / else 的問題,但我覺得這樣不是個一勞永逸地好解法。所以也不特地貼上來給大家參考。為什麼這麼說? 你想嘛,如果哪天鍵盤改變了配置,負責處理 className 和 handleButtonClick
都要動,如果多了 3 種功能,以上兩個函式都要「進去修改」,可以說是違反了「開放封閉」原則。
那可以怎麼改?就回頭改指令,改以剛剛想到的「多型」這個方式來處理,提供以下指令:
以下的 <button ... > 有 Code Smell,可以幫我試著用「多型」的模式重構嗎
給出了不錯滿意的結果,雖然沒有「小步」調整,但跟 AI 協作重構,似乎也不用管這麼多,再加上有測試的保護,不用太害怕:
interface ButtonProps {
label: string;
onClick: () => void;
className?: string;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, className }) => {
const buttonClass = `p-4 text-xl font-bold rounded ${className}`;
return (
<button className={buttonClass} onClick={onClick}>
{label}
</button>
);
};
const Keypad: React.FC<KeypadProps & { currentAmount: string }> = ({
onInput,
onClear,
onBackspace,
onOk,
currentAmount
}) => {
const buttons: ButtonProps[] = [
{ label: "7", onClick: () => onInput("7") },
{ label: "8", onClick: () => onInput("8") },
{ label: "9", onClick: () => onInput("9") },
{ label: "AC", onClick: onClear, className: "bg-orange-500 text-white" },
{ label: "4", onClick: () => onInput("4") },
{ label: "5", onClick: () => onInput("5") },
{ label: "6", onClick: () => onInput("6") },
{ label: "⌫", onClick: onBackspace, className: "bg-yellow-500 text-white" },
{ label: "1", onClick: () => onInput("1") },
{ label: "2", onClick: () => onInput("2") },
{ label: "3", onClick: () => onInput("3") },
{ label: ".", onClick: () => onInput(".") },
{ label: "0", onClick: () => onInput("0") },
{ label: "+", onClick: () => onInput("+"), className: "bg-gray-600 text-white" },
];
return (
<div>
<div className="bg-black p-4">
<div className="text-5xl font-bold text-white text-right">${currentAmount}</div>
</div>
<div className="grid grid-cols-4 gap-2 bg-gray-800 p-4 rounded-b-lg">
{buttons.map((btn, index) => (
<Button
key={index}
label={btn.label}
onClick={btn.onClick}
className={btn.className || "bg-gray-700 text-white"}
/>
))}
<Button
label="OK"
onClick={onOk}
className="bg-blue-500 text-white row-span-2 col-start-4 row-start-3"
/>
</div>
</div>
);
};
跑了測試,很好! 測試都通過了!先 commit 起來,推上遠端!
寫完收工!(並沒有)
下一個是重構「重複邏輯」,把上面列的 Todo 搬下來 👇
handleEdit
和 handleOk
這些 function 內的邏輯,有些地方重複看了一下,handleOk
的部分最適合先拿它開刀 👨⚕️,其中的怪味道是:「過多的 if / else (兩層就算不少了),而且有重複的程式碼」
const handleOk = () => {
const amount = parseFloat(currentAmount);
if (!isNaN(amount)) {
if (isEditingAmount) {
setPendingAmount(amount);
setIsEditingAmount(false);
setShowCategorySelector(true);
} else {
setPendingAmount(amount);
setShowCategorySelector(true);
}
}
setCurrentAmount("0");
};
首先,我先用 early return 的方式進行重構,將第一個 if 嵌套給解開,在該 handleOk
的地方下了指令後,給了我以下結果。不只將 if 解開,還把重複沒用到的程式碼砍了,變得如此精簡,很棒!此外,還看到 isEditingAmount
這個 state 根本在 App 中沒用到,先砍了再說!
按了測試跑一下,全都綠燈通過,看來是沒啥問題 👍
const handleOk = () => {
const amount = parseFloat(currentAmount);
if (isNaN(amount)) return;
setPendingAmount(amount);
setShowCategorySelector(true);
setCurrentAmount("0");
};
調整這個 handleOk
的方法之後,跟 handleEdit
就沒有什麼重複的地方了(或者可以說,抽了共用也沒什麼意思),可喜可賀。
最後,還剩下有同樣症頭的 handleSelectCategory
,這個 if 有兩層,看了頭有點痛。
const handleSelectCategory = (category: string) => {
if (pendingAmount !== null) {
if (editingItem) {
setHistory(history.map(item =>
item.id === editingItem.id ? { ...item, amount: pendingAmount, category } : item
));
setEditingItem(null);
} else {
const newItem = { amount: pendingAmount, category, id: Date.now() };
setHistory([...history, newItem]);
}
setPendingAmount(null);
}
setShowCategorySelector(false);
};
先一樣下指令,把第一個 if 嵌套,用 early return 給去除掉。
const handleSelectCategory = (category: string) => {
setShowCategorySelector(false);
if (pendingAmount === null) return;
if (editingItem) {
setHistory(history.map(item =>
item.id === editingItem.id ? { ...item, amount: pendingAmount, category } : item
));
setEditingItem(null);
} else {
const newItem = { amount: pendingAmount, category, id: Date.now() };
setHistory([...history, newItem]);
}
setPendingAmount(null);
};
接著,我看裡面這個 if(editingItem) 有點不順眼,因為其中做的事情蠻像的,差別只是在於 setHistory 裡面塞的 history ,會因為是不是在「編輯中」而有所不同,於是將這部分也跟著重構了一下。(一樣小提醒,記得要先把上一個重構給 commit 起來呦)
const handleSelectCategory = (category: string) => {
if (pendingAmount === null) return;
const updatedHistory = editingItem
? history.map(item =>
item.id === editingItem.id ? { ...item, amount: pendingAmount, category } : item
)
: [...history, { amount: pendingAmount, category, id: Date.now() }];
setHistory(updatedHistory);
setEditingItem(null);
setPendingAmount(null);
setShowCategorySelector(false);
};
這樣就算重構得差不多,至於資料邏輯做封裝什麼的,是還可以再優化一下,但已經相比原先的寫法,可讀性好非常多了呀。而且測試一樣也都是通過的呦 🟢(再次強調)
所以這部分先到此為止囉 :)。盤點這次做了哪些重構:
Keypad
部分的按鍵處理部分,if / else 巢狀結構過深handleEdit
和 handleOk
這些 function 內的邏輯,有些地方重複以上兩個散發怪味道的程式碼,都用重構的方式給處理掉了,而且在重構過程中,藉由重構的「形塑之力」,原本沒發現到的可優化之處也浮現出來,一併解決掉了 💪。
回顧一下這次做的重構,一個是「重複邏輯」異味的處理,用了 early return 就差不多解決了。
另一個是 KeyPad 的「過深巢狀結構」異味,用了「多型」的方式來重構一番,算是跨步比較大的重構,解決得比較徹底,且整體程式碼易讀性高很多。