Day 06 講過依賴方向規則為什麼容易被忽略——它不會讓測試變紅,也不會讓程式碼跑不起來。今天用一個具體案例,走完一次違反這條規則後,代價實際上是怎麼發生的:不是當下爆炸,而是幾週後,另一個完全不相干的任務,被這個捷徑卡住了。
情境是這樣:系統裡有一個底層的「金額格式化」工具函式,原本只負責把數字轉成帶千分位的字串。某次修 bug 時,需求是「格式化的時候,如果使用者的介面語系設定是某幾種特定語系,千分位符號要跟著換」。
這個語系設定,原本存在一個屬於「使用者偏好設定」模組的物件裡——那是一個明確的上層模組,底下管理使用者的通知偏好、介面主題這些跟使用者互動相關的設定。AI 接到這個任務時,最短路徑很直接:讓格式化工具函式直接 import 使用者偏好設定模組,取出語系欄位,加一個 switch 判斷就解決了。
這個修法當下能動,測試也綠燈——測試給定一個使用者偏好設定物件,驗證格式化結果符合預期,完全沒有問題。
用一組對照來看這個決定:
❌ 順手捷徑:底層格式化工具直接依賴上層使用者偏好設定
namespace App\Formatting;
use App\UserPreference\UserPreferenceRepository; // 底層依賴了上層!
class MoneyFormatter
{
public function __construct(
private UserPreferenceRepository $preferences
) {}
public function format(float $amount, int $userId): string
{
$locale = $this->preferences->findByUserId($userId)->locale;
return $this->formatWithLocale($amount, $locale);
}
}
→ 測試只需要塞一個假的使用者偏好設定就能過,
但 Formatting 這個「應該是最底層、最通用」的工具模組,
現在跟 UserPreference 模組綁死了
✅ 依賴方向正確:呼叫端把需要的資訊傳進來
namespace App\Formatting;
class MoneyFormatter
{
public function format(float $amount, string $locale): string
{
return $this->formatWithLocale($amount, $locale);
}
}
→ Formatting 模組不需要知道「語系是從哪個模組查出來的」,
呼叫端(不管是網頁請求、批次任務、還是報表產生器)
自己負責把 locale 準備好再傳進來
這個捷徑上線後,沒有任何人注意到它——直到三週後,另一個團隊要做一個完全獨立的批次任務:定期產生財務報表,把系統裡累積的金額資料格式化成報表檔案。這個批次任務跑在背景排程裡,沒有「目前登入的使用者」這個概念,自然也拿不到任何使用者的偏好設定。
這個團隊原本以為可以直接重用 MoneyFormatter,畢竟這就是系統裡負責格式化金額的地方——結果發現這支類別的建構子綁死了 UserPreferenceRepository,而批次任務的執行環境裡根本沒有「使用者」這回事,硬塞一個假的使用者 ID 進去顯得很怪,也完全不符合語意。最後這個團隊選擇另外寫一段一模一樣的格式化邏輯,繞過 MoneyFormatter,因為那比搞清楚怎麼在批次環境裡生出一個合法的使用者偏好設定物件要快。
這正是反向依賴的典型代價:它不會讓最初那個任務失敗,卻讓「格式化金額」這件事在系統裡,從一個可以被任何地方安心呼叫的工具,變成一個綁死在特定情境(有登入使用者)的東西——重複實作,就是這個限制的直接後果。
在那個當下,AI 判斷「這樣改能不能解決眼前的問題」是對的——需求確實被滿足了,測試也確實驗證了正確的行為。它沒有、也沒辦法預判到「三週後會有另一個團隊,在一個完全沒有使用者概念的情境下,想重用這段邏輯」。這不是 AI 能力不足的問題,是這類判斷本質上需要對系統未來的使用情境有預期,而這種預期通常不寫在任何一份需求文件裡。
依賴方向規則的價值,正是在這裡填補這個空缺:它不要求 AI(或任何人)預判未來所有可能的重用情境,只要求一條簡單、可以立即檢查的原則——底層工具模組不依賴任何特定的上層業務情境。守住這條規則,「這段邏輯以後能不能在其他情境重用」這個問題,就不需要每次都靠預判來回答,答案是「本來就可以」。
回想你手上系統裡有沒有一段「工具/共用邏輯」,其實悄悄綁死了某個特定情境的物件?如果今天有人想在完全不同的情境下重用它,你有把握這件事能順利做到嗎?
明天要換一個更大的題目:遺留系統的架構重建——手上一套完全沒有清楚邊界的舊系統,該先畫邊界,還是先補測試?兩條路徑各自的風險是什麼。