iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
AI Engineering

AI 寫 Code 之後,我們還需要 Software Architecture 嗎?系列 第 6

Day 06:依賴方向——AI 容易寫出反向依賴,怎麼用架構規則防止

  • 分享至 

  • xImage
  •  

前言:測試都過了,這樣改有什麼問題?

「這個 bug 修好了、測試也綠燈了,為什麼你還要我改掉 import 的方向?」

這是分層架構裡最容易被忽略、卻最傷筋動骨的一種違規。程式能動、測試能過,卻悄悄埋下一個問題:底層模組開始依賴高層模組。今天要講的,就是 AI 為什麼特別容易寫出這種「反向依賴」,以及怎麼用架構規則擋住它。

今日目標

  • 理解依賴方向為什麼是分層架構裡最容易被破壞、卻最少被檢查的一條規則
  • 看清楚 AI 為什麼容易在解一個具體問題時,順手寫出反向依賴
  • 認識依賴反轉原則(Dependency Inversion)跟依賴方向規則的關係
  • 學會用一個具體案例判斷「這是不是反向依賴」

依賴方向:分層架構裡最容易被忽略的一條規則

分層架構通常隱含一條方向規則:高層模組(業務邏輯)不該依賴低層模組(實作細節),兩者都該依賴抽象。這個原則在 Robert C. Martin 提出的 Clean Architecture 裡被講得更明確,稱為「依賴規則」(The Dependency Rule):原始碼的依賴關係只能指向內層,外層的具體實作可以依賴內層的抽象,但內層的抽象不能反過來依賴外層的具體實作。

這條規則在文件裡寫起來很乾脆,但它不是一條編譯器會幫你檢查的規則——你完全可以寫出一段違反依賴方向、卻能正常編譯、正常執行、測試也綠燈的程式碼。這正是它容易被忽略的原因:沒有任何工具在你破壞它的當下發出警告。

AI 為什麼容易寫出反向依賴

想像一個情境:某個底層的資料驗證模組,需要根據「目前登入的使用者角色」決定要不要套用某條驗證規則。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 模組該關心的事。

這兩個版本的行為完全一樣、測試都能綠燈,差別只在「誰依賴誰」——但這條看不出來的線,決定了這個模組未來能不能被獨立抽出、獨立測試、獨立重用。

為什麼這件事對 AI 協作特別重要

呼應這個系列從 Day 01 就開始講的命題:AI 對一段程式碼的修改,如果只依據「這樣改能不能解決眼前的問題」來判斷安不安全,它永遠看不到依賴方向這種需要整體架構視角才看得出來的違規。一個底層模組多了一條指向上層的 import,不會讓任何一個現有測試變紅——它只會在未來某一天,當你想把這個底層模組抽出來獨立使用、或者想幫它寫一個不依賴上層服務的單元測試時,突然發現「怎麼抽不出來」。

依賴方向規則的價值,正是在填補 AI 判斷力天生的盲區:AI 擅長判斷「這樣改測試會不會過」,不擅長判斷「這樣改會不會讓這個模組以後更難獨立存在」——而後者恰好是架構規則該負責的事。

要讓這件事真的能被檢查出來,光靠 code review 時人工肉眼掃過 import 陳述式是不夠的(人也一樣容易漏看)。比較實際的做法是把依賴方向規則寫成可以自動檢查的規則(例如限制某個目錄下的檔案不能 import 另一個目錄的類別),這件事會在後面第三部詳細講怎麼做。

今日思考題

回想你手上專案裡某個「底層」模組(例如驗證邏輯、工具函式、資料存取層):它有沒有 import 任何理論上屬於「上層」的東西?如果有,這條依賴是刻意設計的,還是像今天案例那樣,是為了解一個當下的問題而順手加上去的?

今日重點回顧

  • 依賴方向規則規定高層不該依賴低層的具體實作、兩者都該依賴抽象,但這條規則沒有編譯器或測試會自動幫你檢查
  • AI 判斷一個修法安不安全,依據的是「能不能解決眼前問題」,這個判斷天生看不到依賴方向這種架構層次的違規
  • 反向依賴的典型樣貌:底層模組為了拿到某個資訊,直接 import 了上層服務,而不是讓呼叫端把資訊傳進來
  • 反向依賴的代價不會立刻出現,而是在未來想獨立抽出/獨立測試這個模組時才顯現

明日預告

第一部最後一天:模組邊界——package/module 隔離對 AI 改動範圍的實際影響,把這幾天講的介面切分、依賴方向這些抽象概念,收斂成一個具體的、可以直接畫在資料夾結構上的邊界工具。第二部從 Day 8 開始,會用一連串案例看「沒有這些架構約束時,AI 實際上會怎麼寫」。


上一篇
Day 05:介面切分——為什麼「型別提示介面 vs 具體類別」是架構決策
系列文
AI 寫 Code 之後,我們還需要 Software Architecture 嗎?6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言