過去幾天談的 OCP、DIP、模組化、介面抽象、錯誤處理,都是「理想中程式碼該有的樣子」。但現實中大部分程式碼都是先求能動、再求好——今天要談的重構,就是把已經存在、能動但不夠理想的程式碼,逐步調整成前面幾天描述的樣子,而不需要把整個系統推倒重寫。
Martin Fowler 的定義是:重構是「在不改變軟體外部可觀察行為的前提下,調整其內部結構,讓它更容易被理解、日後修改的成本更低」的過程。
關鍵字是「外部可觀察行為不變」——同樣的輸入,重構前後都必須得到同樣的輸出。這正是重構跟「改功能」、「修 bug」最根本的差異:改功能和修 bug 都是在改變系統的行為,而重構只改變程式碼的內部結構,不改變它對外呈現的結果。
Fowler 強調,重構是「一連串很小、而且每一步都會保留原本行為的轉換(transformation)」。因為每一步改動都很小,出錯的機率也隨之降低——就算某一步真的改壞了,也很容易回頭找出是哪一步造成的,而不是要在一大片改動裡大海撈針。
測試扮演的角色:在每一個小步驟之後,重新執行測試,確認外部行為真的沒有改變,才進行下一步。沒有測試網的重構,等於是在沒有安全繩的狀態下走鋼索——這也是為什麼「先補測試,再重構」常常是實務上的第一步。
延續 Day 13 的義大利麵程式碼範例(processOrder 同時做了商品抓取、訂單計算、資料庫連線、Line 通知四件事),示範怎麼用重構的方式,一步一步走向 Day 14 分層架構、Day 15 SRP 的最終樣貌,而不是直接重寫整個系統。
// 重構前:義大利麵程式碼(Day 13 的範例)
async function processOrder(order) {
const item = await fetchProduct(order.itemId);
const total = item.price * order.qty + order.shippingFee;
await db.query(`UPDATE orders SET total = ${total} WHERE id = ${order.id}`);
await lineNotify(order.buyerId, `您的訂單總額為 ${total} 元`);
}
// 第一步重構:把「計算總金額」抽成獨立函式,行為完全不變,只是換了位置
function calculateTotal(item, order) {
return item.price * order.qty + order.shippingFee;
}
async function processOrder(order) {
const item = await fetchProduct(order.itemId);
const total = calculateTotal(item, order); // 呼叫端拿到的結果跟重構前一模一樣
await db.query(`UPDATE orders SET total = ${total} WHERE id = ${order.id}`);
await lineNotify(order.buyerId, `您的訂單總額為 ${total} 元`);
}
// 這一步做完,先重新跑一次測試,確認輸出結果沒有改變,才進行下一步
// 第二步重構:把資料庫存取也抽成獨立函式
async function saveOrderTotal(orderId, total) {
await db.query(`UPDATE orders SET total = ${total} WHERE id = ${orderId}`);
}
async function processOrder(order) {
const item = await fetchProduct(order.itemId);
const total = calculateTotal(item, order);
await saveOrderTotal(order.id, total);
await lineNotify(order.buyerId, `您的訂單總額為 ${total} 元`);
}
// 重複這個「抽出一小塊、跑測試、確認行為不變」的過程,
// 最終才會走到 Day 14 分層架構、Day 15 SRP 看到的那個各司其職的乾淨版本
每一步重構做的事情都很小——只是把一段程式碼「搬」到一個新的函式裡,呼叫端的輸出結果完全沒有變。但這些小步驟累積起來,最終就能把一開始糾結成一團的 processOrder,逐步拆成前幾天談到的分層架構與符合 SRP 的獨立模組。
重構不是一次性的大改版,而是用一連串「外部行為不變、內部結構變好」的小步驟,把程式碼慢慢帶往 OCP、DIP、SRP 所描述的理想狀態。這也回頭說明了為什麼這系列會從 Day 13 的壞味道,一路講到 Day 15 的 SRP,再到這幾天的 OCP、DIP、模組化——它們不只是抽象的原則,更是重構時「該往哪個方向走」的具體指引。