「這個 bug 修好了、測試也綠燈了,為什麼你還要我改掉 import 的方向?」
這是分層架構裡最容易被忽略、卻最傷筋動骨的一種違規。程式能動、測試能過,卻悄悄埋下一個問題:底層模組開始依賴高層模組。今天要講的,就是 AI 為什麼特別容易寫出這種「反向依賴」,以及怎麼用架構規則擋住它。
分層架構通常隱含一條方向規則:高層模組(業務邏輯)不該依賴低層模組(實作細節),兩者都該依賴抽象。這個原則在 Robert C. Martin 提出的 Clean Architecture 裡被講得更明確,稱為「依賴規則」(The Dependency Rule):原始碼的依賴關係只能指向內層,外層的具體實作可以依賴內層的抽象,但內層的抽象不能反過來依賴外層的具體實作。
這條規則在文件裡寫起來很乾脆,但它不是一條編譯器會幫你檢查的規則——你完全可以寫出一段違反依賴方向、卻能正常編譯、正常執行、測試也綠燈的程式碼。這正是它容易被忽略的原因:沒有任何工具在你破壞它的當下發出警告。
想像一個情境:某個底層的資料驗證模組,需要根據「目前登入的使用者角色」決定要不要套用某條驗證規則。AI 被要求修這個 bug 時,最短路徑往往是:直接讓這個底層驗證模組去 import 上層的使用者身份服務,取得角色資訊,加一個 if 判斷就解決了。
這個修法當下能動、測試也能過——因為測試通常只驗證「這個輸入現在有沒有正確地被擋下來」,不會驗證「這個模組的依賴方向有沒有被破壞」。AI 判斷一個修法對不對,依據的是「這樣改能不能解決眼前的問題」,而依賴方向這種架構層次的違規,不會直接反映在任何一個測試的紅綠燈上。
用一組對照來看這個差異:
❌ 反向依賴:底層驗證模組直接依賴上層身份服務
namespace App\Validation;
use App\Identity\UserSessionService; // 底層依賴了上層!
class DiscountRuleValidator
{
public function __construct(
private UserSessionService $session
) {}
public function validate(Order $order): bool
{
$role = $this->session->getCurrentUserRole();
// 根據角色決定驗證邏輯
return $role === 'vip'
? $order->amount >= 0
: $order->amount >= 100;
}
}
→ 這樣寫測試也能過(給定一個 session mock 就行),
但 Validation 模組現在跟 Identity 模組綁死了。
以後想把 Validation 抽出去獨立套件、獨立測試、
甚至換到另一個完全不需要使用者身份概念的情境使用,
都會被這個依賴卡住。
✅ 依賴方向正確:透過抽象傳入所需資訊,不依賴上層具體服務
namespace App\Validation;
class DiscountRuleValidator
{
public function validate(Order $order, string $customerTier): bool
{
return $customerTier === 'vip'
? $order->amount >= 0
: $order->amount >= 100;
}
}
→ Validation 模組不需要知道「使用者角色從哪裡來」,
只需要呼叫端把它需要的資訊(customerTier)傳進來。
誰負責決定 customerTier 怎麼算出來,
是上層(呼叫這個 validator 的地方)的責任,
不是 Validation 模組該關心的事。
這兩個版本的行為完全一樣、測試都能綠燈,差別只在「誰依賴誰」——但這條看不出來的線,決定了這個模組未來能不能被獨立抽出、獨立測試、獨立重用。
呼應這個系列從 Day 01 就開始講的命題:AI 對一段程式碼的修改,如果只依據「這樣改能不能解決眼前的問題」來判斷安不安全,它永遠看不到依賴方向這種需要整體架構視角才看得出來的違規。一個底層模組多了一條指向上層的 import,不會讓任何一個現有測試變紅——它只會在未來某一天,當你想把這個底層模組抽出來獨立使用、或者想幫它寫一個不依賴上層服務的單元測試時,突然發現「怎麼抽不出來」。
依賴方向規則的價值,正是在填補 AI 判斷力天生的盲區:AI 擅長判斷「這樣改測試會不會過」,不擅長判斷「這樣改會不會讓這個模組以後更難獨立存在」——而後者恰好是架構規則該負責的事。
要讓這件事真的能被檢查出來,光靠 code review 時人工肉眼掃過 import 陳述式是不夠的(人也一樣容易漏看)。比較實際的做法是把依賴方向規則寫成可以自動檢查的規則(例如限制某個目錄下的檔案不能 import 另一個目錄的類別),這件事會在後面第三部詳細講怎麼做。
回想你手上專案裡某個「底層」模組(例如驗證邏輯、工具函式、資料存取層):它有沒有 import 任何理論上屬於「上層」的東西?如果有,這條依賴是刻意設計的,還是像今天案例那樣,是為了解一個當下的問題而順手加上去的?
第一部最後一天:模組邊界——package/module 隔離對 AI 改動範圍的實際影響,把這幾天講的介面切分、依賴方向這些抽象概念,收斂成一個具體的、可以直接畫在資料夾結構上的邊界工具。第二部從 Day 8 開始,會用一連串案例看「沒有這些架構約束時,AI 實際上會怎麼寫」。