上次小步地重構了兩個部分,腳已經開始有點酸麻。雖然都是請 AI 幫忙寫,但還是沒有很輕鬆,因為要找問題點在哪,而且最後的解方也是我想的,看這次有沒有更好的重構法囉?
謎之聲:叫你阿罵來囉
好了,之前留下來的 Todo,還剩下 2 個。
App
共有 7 個狀態(變 6 個了),有點太多,不好理解handleInput
部分沒有處理阻擋不能輸入的部分同樣按部就班,一個個解決掉 Todo ,分別是重構和功能這兩項,那當然是先做重構囉。
好了,該重構的項目剩下一個(如果忘記為什麼是一個的,請看上一篇),這個有點難一下子解決,到底有什麼辦法可以削減 state 的數量呢?
那有什麼比較通用的原則,可以作為削減 state 的依據呢?有的,我們可以用「不變數據」這個數據原則做延伸:「不需要狀態變化的,就不要用 state 存起來」
換句話說,如果只是計算值而已,那不用特地用 useState
來儲存值的變化。
啊!這樣說來,有看到一個 ! 就是這個 total 的值
const [total, setTotal] = useState<number>(0);
//...
useEffect(() => {
const newTotal = history.reduce((sum, item) => sum + item.amount, 0);
setTotal(newTotal);
}, [history]);
從以上表達式來看,我根本沒必要特地存一個 history 的派生變化值(Derived Value),直接寫成查詢(query)即可,這樣就可以消滅掉一個 state 了!輸入指令請 AI 重構後,total 直接從 history 即時計算出來即可。(備註:可參考「重構:改善既有的程式」一書中的 查詢取代派生
)
跑一下測試,很好,一樣也是測試都通過了。
const total = useMemo(() => {
return history.reduce((sum, item) => sum + item.amount, 0);
}, [history]);
下一個動的是 currentAmount
和 pendingAmount
這兩個 state,看起來是沒必要分兩種 amount 去儲存,為了驗證我的猜想,我快速請 AI 幫我重構看看:
可以幫我把 App 中的 state 給簡化嗎? 尤其是 currentAmount 和 pendingAmount 看起來用途相當接近耶
咻地一下子,AI 幫我做好了重構,快速看了一下,看起來沒什麼大問題…才對。於是乎,也是一樣先跑個測試驗證一下,哎?
測試派去了… 🤯 壞在 allows editing of an existing record by double-clicking
這項測試,也就是說,沒有辦法正常編輯已經添加上去的帳目。
先不說有沒有跑比較快,測試被弄壞了,既然是只有「編輯」這一項測試壞掉,那應該就是出錯在 handleEdit
這一塊,但也看不出什麼所以然… 🤔 只好 undo 退回上一步,把 Apply 的結果給叫出來,如以下所示:
- const handleEdit = (id: number, amount: number, category: string) => {
- setEditingItem({ id, amount, category });
- setPendingAmount(amount);
- setCurrentAmount(amount.toString());
+ const handleEdit = (id: number, editAmount: number, category: string) => {
+ setEditingItem({ id, amount: editAmount, category });
+ setAmount(editAmount.toString());
+ setShowCategorySelector(true);
+ };
看了一下,原來是 AI 偷偷幫我「加料」,把 setShowCategorySelector(true);
加上去,壞!偷偷來都不揪的!
那把這一行砍掉,照理說應該就行了,再次跑了測試,沒問題,很好!
經歷了以上的重構,App 的狀態銳減剩下 4 個。而且重點是「功能不變」,程式碼的複雜度大幅降低,清爽很多了 👍。
還想提一個可以重構的地方,是 showCategorySelector
這個 true / false 狀態
const [showCategorySelector, setShowCategorySelector] = useState<boolean>(false);
//...
{showCategorySelector ? (
<CategorySelector
onSelectCategory={handleSelectCategory}
onCancel={handleCancelCategorySelection}
pendingAmount={parseFloat(amount)}
initialCategory={editingItem ? editingItem.category : null}
/>
) : (
<Keypad
onInput={handleInput}
onClear={handleClear}
onBackspace={handleBackspace}
onOk={handleOk}
currentAmount={amount}
/>
)}
你可能會想說,這個有什麼問題嗎?不就是個顯示的「開關」而已?
但到底哪個是開,哪個是關,難道不能用 Keypad 是否顯示作為開關嗎? 當然可以。如此說來,這邊根本就不是「開關」,而是個「多選一」的條件渲染,只是到目前為止,這部分「剛好」只有 2 個條件而已。
就順手提供了指令:
幫我把 showCategorySelector 改成「多種」的狀態
- 新增 enum 來儲存要顯示的 pad 組件
- 同步修改下面的渲染邏輯
於是 AI 幫我新增了 enum 和重構相關邏輯,變得更好理解了些。至於最後的三元表達式渲染,是可以調整一下,但…目前 Force 還不夠,所以先這樣就好 👍。
enum PadState {
Keypad,
CategorySelector,
}
//...
const [currentPad, setCurrentPad] = useState<PadState>(PadState.Keypad);
//...
{currentPad === PadState.CategorySelector ? (
<CategorySelector
onSelectCategory={handleSelectCategory}
onCancel={handleCancelCategorySelection}
pendingAmount={parseFloat(amount)}
initialCategory={editingItem ? editingItem.category : null}
/>
) : (
<Keypad
onInput={handleInput}
onClear={handleClear}
onBackspace={handleBackspace}
onOk={handleOk}
currentAmount={amount}
/>
)}
最後還是要提醒一下,如果光是要理解,就有困難了,下次再回頭看還是會忘記 😴
重構的部分都好了,剩下最後一個算是功能的調整:
handleInput
部分沒有處理阻擋不能輸入的部分首先是加上了測試,測試比較明確,是希望輸入部分,只能輸入「浮點數」之數字格式。其他啊仨布魯的奇怪格式,都要幫我擋起來,不給輸入!
新增輸入數字的測試
- 點擊輸入 1.....1,只會顯示 1.1
- 可以輸入 0.1,就會顯示 0.1
最後加上了此項新邏輯(還是得用 claude-sonnet-3.5),並且重構了一番,生成了如下的程式碼,細節步驟就不特地展示了。
const isValidInput = (input: string) => {
const regex = /^(\d*\.?\d*)$/;
return regex.test(input);
};
const handleInput = (value: string) => {
const newAmount = (() => {
if (amount === "0" && value !== ".") return value;
if (value === "." && !amount.includes(".")) return amount + value;
if (value !== ".") return amount + value;
return amount;
})();
if (isValidInput(newAmount)) {
setAmount(newAmount);
}
};
要不是有測試的保護,要直接請 AI 幫忙重構大區塊的程式碼,我還真不敢這麽做。此外,因為先前寫的是「整合測試」,不怕因為程式碼在重構之後消失不見,像是原先的 state 從 7 個減為 4 個,如果當初是做其中的 state 狀態變化「單元測試」,那些單元測試在經過測試的調整之後,就沒路用了。(但也不是說單元測試沒用,要看程式單元而定)
以前都覺得說,我一次只改一點點程式碼,好像就弱掉了。容易很貪心一次改很多東西,除了重構時「不遵守規矩」,常「偷渡」新功能在重構之中,而且一次重構都會改太多,值到改到壞掉才驚覺,原來改動的數量是自己無法掌控的了,這才會停手。
直到踩過多次的坑,才終於痛定思痛改變自己的習慣,以「小不增量」的方式,一次寫一點,一次做一點重構…。
比較好的重構方法,藉著測試的保護之下,一次調整「一塊功能」,調整完之後確保測試通過 🟢,commit 起來(存檔紀錄)。
再次重構調整,若改壞了,測試不通過 🔴,那就先改到好,直到測試通過。若無法解決,那先緩緩,切忌把「測試壞掉」的程式碼做 commit 並且 push 到遠端。
我們提供策略、想法和方法,AI 負責去執行實作。現在跟 AI 協作重構,可以加速重構的速度,但也因此容易「超速」而不小心把程式碼改壞 😂 (就像 藤原拓海 車速太快 就變 藤原填海)
所以當程式碼生成的速度越快,心要越慢,慢下來好好看 AI 生成的結果。
最後的最後,小小抱怨一下,使用 Cursor IDE PRO 版的免費 gpt-4o-mini 模型,在寫新功能方面真的不太行,基本且具體的重構倒是還可以,果然還是只有 claude-sonnet 比較厲害一點。
最近有所耳聞其他 AI 工具,像是 claude dev 聽說相當厲害,有機會再來找來試試看 :)