「SQL 要收斂進 Repository,這個原則我聽懂了。但真的要動手改一個已經在正式環境跑好幾年的 Controller,第一步到底該做什麼?」
昨天講完「為什麼」,今天要老實面對「怎麼做」。這才是 AI 重構 legacy 系統最容易翻車的地方——原則人人都會講,但AI 一旦開始動手,很容易急著把整個方法一次改完,而不是先確認每一步都站在有安全網的地方。今天用一個具體案例,走一遍完整的重構過程。
假設有一個 OrderController::getOrderSummary() 方法,接手時長這樣:
❌ 重構前:Controller 直接組 SQL 查資料庫
class OrderController
{
public function getOrderSummary(int $customerId): array
{
$pdo = new PDO($dsn, $user, $pass);
$stmt = $pdo->prepare(
'SELECT id, amount, status FROM orders WHERE customer_id = ? AND status != ?'
);
$stmt->execute([$customerId, 'deleted']);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
這種寫法在 legacy 系統裡很常見:Controller 直接建立連線、直接組 SQL、直接回傳查詢結果。它能動,但有三個問題疊在一起——資料庫存取邏輯跟 HTTP 請求處理邏輯攪在一起、SQL 字串沒有集中管理沒辦法統一套用可攜性規則、而且這段程式碼有沒有測試,你光看程式碼本身根本看不出來。
我請 AI 重構這個方法時,它第一個動作不是寫程式碼,而是回報這段程式碼目前的測試覆蓋狀態。 這聽起來理所當然,但在還沒建立這套紀律之前,「先確認覆蓋率」常常被跳過——AI 傾向直接看程式碼邏輯,判斷「這段改法應該沒問題」,然後動手。
實際查證後發現:getOrderSummary() 只有一支測試打到,且那支測試只驗證了「回傳的陣列不是空的」,完全沒斷言 status != 'deleted' 這個過濾條件有沒有生效。這代表如果重構過程不小心把過濾條件改壞,現有測試完全抓不到。
於是第一步不是遷移程式碼,而是先補一個會斷言具體過濾行為的測試,把安全網先建起來,再開始動手改。這正是 Day 01 那句話在實戰裡的樣子——如果沒有先確認查證範圍夠不夠,「已經有測試」這個結論本身就可能是假的。
安全網補好之後,才開始把資料庫存取邏輯搬進 Repository:
✅ 重構後:Controller 呼叫 Repository
class OrderRepository
{
public function findActiveByCustomer(int $customerId): array
{
return $this->connection->findWhere('orders', [
'customer_id' => $customerId,
'status' => ['!=', 'deleted'],
]);
}
}
class OrderController
{
public function __construct(private OrderRepository $orders) {}
public function getOrderSummary(int $customerId): array
{
return $this->orders->findActiveByCustomer($customerId);
}
}
這個遷移過程裡有個細節很容易被忽略:如果同一支 Controller 裡還有另一個地方也用類似的方式查 orders 表,但那個地方沒有測試覆蓋,這次不該順手一起改掉。AI 很容易覺得「反正邏輯類似,一起處理比較有效率」,但「效率」的前提,是每一處改動背後都有測試在接住。沒有覆蓋的部分應該留到下一輪、先補測試再處理,而不是趁著這次一起帶過去。
程式碼遷移完,最後一步是跑一次「帶著改動」跟「乾淨基準」兩次測試比對——這是 Day 06 講過的機制,這裡具體套用在這個案例上:確認新舊版本對同一批輸入(有訂單、沒有訂單、全部被刪除、customer_id 不存在)回傳的結果逐筆一致,而不是只看「測試綠燈」就結案。
這整個過程沒有一步是「AI 憑經驗判斷應該沒問題」,每一步的判斷依據都是可以被檢查的具體證據:有沒有測試覆蓋、測試斷言了什麼、新舊行為的比對結果。這正是系列主題句的具體實踐——AI 的自信範圍,被收斂成一連串可以被驗證的小問題,而不是一個籠統的「我覺得這樣改是對的」。
順帶一提,這裡示範用的 findWhere() 條件陣列 DSL、資料庫連線類別,都是 PHP 生態裡的實現方式;換成其他語言,可能對應到 ORM 的 query builder 或參數化查詢介面。但「先確認安全網、一次只搬一件事、遷移完要逐筆比對行為」這套紀律,換語言依然成立。
回想你上一次把資料庫存取邏輯搬出 Controller 的經驗:你是先查了覆蓋率才動手,還是先動手改完才想到「這裡好像沒測試」?如果是後者,那次重構其實是在賭運氣,只是剛好沒賭輸而已。
明天要把視角從「站內查資料庫」轉到「呼叫站外 API」——外部廠商的 API 呼叫,怎麼從裸 curl 收斂成用 PSR-17/PSR-18 介面組出來的呼叫,讓測試可以換成 mock client,而不必真的打一次網路。