iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Claude AI

用 AI Agent 重構一套無框架的 legacy PHP 系統系列 第 9

Day 09:Repository 重構實戰——一個 Controller 直接查資料庫的案例

  • 分享至 

  • xImage
  •  

前言:道理都懂,動手時卡在哪裡?

「SQL 要收斂進 Repository,這個原則我聽懂了。但真的要動手改一個已經在正式環境跑好幾年的 Controller,第一步到底該做什麼?」

昨天講完「為什麼」,今天要老實面對「怎麼做」。這才是 AI 重構 legacy 系統最容易翻車的地方——原則人人都會講,但AI 一旦開始動手,很容易急著把整個方法一次改完,而不是先確認每一步都站在有安全網的地方。今天用一個具體案例,走一遍完整的重構過程。

今日目標

  • 看一個「Controller 直接組 SQL 查資料庫」的具體案例,從頭到尾走一次重構流程
  • 理解「先查覆蓋率、再動手」在真實案例裡具體長什麼樣子
  • 認識「同一個舊寫法出現兩次,但只有一處被測試覆蓋」這種容易被 AI 忽略的細節
  • 掌握遷移完成後怎麼驗證行為完全一致,而不是「看起來能動就好」

案例:一個藏在 Controller 裡的訂單查詢

假設有一個 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,一次只搬一件事

安全網補好之後,才開始把資料庫存取邏輯搬進 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 的經驗:你是先查了覆蓋率才動手,還是先動手改完才想到「這裡好像沒測試」?如果是後者,那次重構其實是在賭運氣,只是剛好沒賭輸而已。

今日重點回顧

  • 重構前先查覆蓋率,覆蓋不到的假設要先補測試,不能憑「邏輯看起來沒問題」就動手
  • 同一個舊寫法出現兩次,只改被測試覆蓋到的那一處,不要因為「反正類似」就一起改掉
  • 遷移完成不是測試綠燈就結案,要逐筆比對新舊行為在各種輸入下是否完全一致
  • 這整套流程把「AI 憑經驗判斷」收斂成一連串可驗證的具體問題,呼應系列主題句
  • 具體工具(條件陣列 DSL、PDO)是 PHP 的實現方式,但紀律本身語言無關

明日預告

明天要把視角從「站內查資料庫」轉到「呼叫站外 API」——外部廠商的 API 呼叫,怎麼從裸 curl 收斂成用 PSR-17/PSR-18 介面組出來的呼叫,讓測試可以換成 mock client,而不必真的打一次網路。


上一篇
Day 08:SQL 一律經過 Repository——把裸寫 SQL 的 legacy 程式碼收斂
下一篇
Day 10:外部 API 呼叫的介面化——從裸 curl 到 PSR-17/PSR-18
系列文
用 AI Agent 重構一套無框架的 legacy PHP 系統10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言