iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

當 AI 寫得比你讀得快:Code Review 該審什麼系列 第 11 篇

Day 11:案例拆解——多出來的 Repository 裝飾器鏈跟 UnitOfWork 在保護什麼

  • 分享至 

  • xImage
  •  

前言:「有 Retry、有 Transaction,聽起來至少比較安全吧?」

如果你 review 到一段程式碼,類別名稱叫 RetryingArticleRepositoryDecorator、TransactionalArticleRepositoryDecorator,建構子還宣告了 MAX_ATTEMPTS = 3 這種常數,大概率會覺得「這個系統的容錯跟一致性有人認真處理過」,直接放行。

今天要拆給你看:版本 B 裡這兩個類別,名字承諾的能力,跟它們實際的行為,是兩件事。這正是這個系列反覆強調的核心風險——AI 產出的程式碼,很容易讓「架構意圖」讀起來很完整,但沒有人真的去驗證「意圖」跟「行為」是不是同一回事。

今日目標

  • 看懂 Decorator Pattern(裝飾器模式)跟 Unit of Work Pattern 各自原本要解決的問題
  • 用兩段真實程式碼具體理解「名字承諾的能力」跟「實際行為」為什麼會脫鉤
  • 認識 PDO 的交易 API(beginTransaction/commit/rollBack)沒有被呼叫,代表什麼
  • 學會 review 這類「非功能性」裝飾器時該問的第一個問題
  • 理解「同一條規則被檢查三次」這種重複,是職責劃分不清楚的徵兆

案例 1:SqliteUnitOfWork 完全沒有交易保護

Unit of Work Pattern 的原始用意,是把一次操作涉及的多筆資料庫寫入包在同一個交易邊界裡,確保「要嘛全部成功、要嘛全部回滾」。版本 B 裡確實有一個實作了 UnitOfWorkInterface 的 SqliteUnitOfWork:

class SqliteUnitOfWork implements UnitOfWorkInterface
{
    public function begin(): void   { $this->logger->log('SqliteUnitOfWork: begin (no-op)'); }
    public function commit(): void  { $this->logger->log('SqliteUnitOfWork: commit (no-op)'); }
    public function rollback(): void{ $this->logger->log('SqliteUnitOfWork: rollback (no-op)'); }
}

begin()、commit()、rollback() 三個方法都只寫了一行 log,從未呼叫 PDO 的 beginTransaction()/commit()/rollBack()。但 PublishArticleService 的每一個操作都包了一層 runInTransaction(),實際呼叫的就是這個類別:

private function runInTransaction(\Closure $operation): mixed
{
    $this->unitOfWork->begin();
    try {
        $result = $operation();
        $this->unitOfWork->commit();
        return $result;
    } catch (\Throwable $exception) {
        $this->unitOfWork->rollback();
        throw $exception;
    }
}

版本 B 原始碼裡這個類別自己的註解寫得很直白:

名為「SQLite Unit of Work」,實際上不對 PDO 連線呼叫任何 beginTransaction/commit/rollBack……它只留下「看起來嚴謹」的介面與 log,卻沒有提供任何實際的交易保護。

❌ 只看類別名稱與介面的判斷:

implements UnitOfWorkInterface
+ begin()/commit()/rollback() 三個方法都在
= 這裡有交易保護

✅ 該追問的問題:

這三個方法裡面,
真的呼叫了資料庫的交易 API 嗎?
還是只是 log 了一句話?

如果這個系統將來真的需要跨多筆寫入的一致性保證,這裡完全沒有提供——這比「乾脆沒有 UnitOfWork」更危險,因為它讓人誤以為保護已經存在。 讀程式碼的人如果只看到介面實作齊全,很容易在後續開發時假設「反正有 UnitOfWork 罩著」,而不會另外加防護。

案例 2:RetryingArticleRepositoryDecorator 永遠只試一次

Decorator Pattern(裝飾器模式)的用途,是在不改動原始物件介面的前提下,動態疊加額外行為——這裡本來的用意應該是「資料庫暫時性錯誤時自動重試」。版本 B 裡的實作:

private const MAX_ATTEMPTS = 3;

private function withRetry(\Closure $operation): mixed
{
    $attempt = 1;
    $this->logger->log("RetryingArticleRepositoryDecorator: attempt {$attempt}/".self::MAX_ATTEMPTS);

    return $operation();
}

宣告了 MAX_ATTEMPTS = 3,方法名字叫 withRetry,但邏輯裡沒有迴圈、沒有 catch、沒有真的重試第二次——$attempt 永遠是 1,$operation() 只被呼叫一次。這是很典型的「架構意圖」跟「實際行為」脫鉤的樣貌:讀 class 名稱、讀常數,會以為系統有容錯能力,但它沒有。

這兩個案例合起來看,反映的是同一個問題:當程式碼的產出速度快到來不及逐行驗證時,「看起來像」什麼,很容易被誤認成「就是」什麼。 這正是這個系列的主題句:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。 審的重點不能停留在「這個類別是不是照著模式的介面寫的」,而要往前一步問「產生這段程式碼的規則,有沒有要求它的行為跟名字一致」。

補充案例:同一條規則被檢查三次

「標題不可為空」這條規則,在版本 B 裡同時存在於 TitleNotEmptySpecification::isSatisfiedBy()、呼叫它的 CreateArticleValidator::validate(),以及又呼叫了一次同一個 Specification 的 ArticleAggregate 建構子。三個地方都會拋出同一種例外,行為上不衝突,但如果將來要改這條規則(例如加上標題長度上限),需要同時記得改三個地方——這正是 Validator/Specification/Aggregate 三層各自負責什麼職責劃分不清楚時,最容易發生的重複。

今日思考題

回想你 review 過的程式碼裡,有沒有類似「類別名字承諾了某個能力,但你沒有真的去讀實作內容確認它做到了」的情況?你覺得要怎麼樣的 review 習慣,才能系統性地抓出這種「名不符實」?

今日重點回顧

  • SqliteUnitOfWork 的 begin()/commit()/rollback() 都只是 log,從未呼叫 PDO 的交易 API
  • RetryingArticleRepositoryDecorator 宣告了 MAX_ATTEMPTS = 3,但邏輯上永遠只執行一次
  • 兩者共同的問題是「架構意圖」與「實際行為」脫鉤,類別名字比實作內容更容易被信任
  • 「標題不可為空」這條規則同時存在於 Specification/Validator/Aggregate 三層,是修改成本被放大的隱形風險

明日預告

Day 12 會拆解版本 B 裡的 Event/Listener 機制——這套聽起來很「事件驅動」的架構,實際上解決的是一個根本不存在的問題。

老派工程師的心得

我自己在職涯早期,也寫過類似 withRetry 這樣「先把介面掛上去,邏輯之後再補」的程式碼,通常是因為當下沒時間、想著「先讓 code review 過,之後再補完整」。這次拆解版本 B 才意識到,AI 生成程式碼時完全沒有「之後再補」的自覺——它一次寫完一整個類別,名字、常數、方法簽名全部到位,讀起來像是完工的樣子,但邏輯是空的。這種「完工感」比人手寫的半成品更容易騙過 review,因為它連該有的破綻(TODO 註解、明顯的簡化)都沒有留下。


上一篇
Day 10:版本 B——同一組測試,AI 自由發揮寫出來的過度設計版本
下一篇
Day 12:案例拆解——Event/Listener 機制解決了不存在的問題
系列文
當 AI 寫得比你讀得快:Code Review 該審什麼 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言