「模組邊界不就是把程式碼分到不同資料夾嗎?這個系列前幾天不是已經講過 Repository 分層、介面切分了嗎,模組邊界是同一件事吧?」
這個問題問得好,也正好點出一個容易混淆的地方。Repository 分層講的是「資料存取邏輯收斂到一個地方」,介面切分講的是「呼叫端跟實作之間隔著一層契約」——這兩天都是在講單一依賴關係的邊界。今天要講的模組/package 邊界,尺度更大:它決定的不是一個類別跟另一個類別的關係,而是一整群類別,作為一個單元,能不能被獨立驗證、獨立改動,不會被外面的世界悄悄影響。
把程式碼分到不同資料夾很容易,任何人都能做到——建幾個目錄,把相關的檔案搬進去。但資料夾邊界不等於依賴邊界:如果 A 資料夾裡的程式碼,可以隨意 import/use B 資料夾裡任何東西,沒有任何機制檢查這件事,那麼這兩個資料夾在依賴關係上其實是同一坨,只是視覺上分開放而已。
真正的模組邊界,看的是依賴宣告有沒有被強制:這個模組對外暴露什麼、依賴什麼,是不是寫死在一份清楚的清單裡(例如獨立的 composer.json、package.json,或語言原生的 module 系統),而不是「反正同一個專案,想用什麼就用什麼」。
用一組對照來看這個差異:
❌ 邊界模糊(資料夾分開,依賴沒有強制):
modules/billing/Invoice.php
modules/reporting/ReportBuilder.php
// ReportBuilder 裡直接 use Billing\Invoice;
// 沒有任何清單宣告 reporting 依賴 billing,
// 也沒有測試在邊界被打破時會失敗
→ AI 要修改 billing 模組內部的某個邏輯時,
沒辦法只憑「這是 billing 資料夾」就判斷影響範圍,
必須整個專案 grep 一次「還有誰在 use Billing 底下的東西」,
而且 grep 找到的,只是「目前看得到的依賴」,
不保證找全
✅ 邊界清楚(依賴宣告被強制):
billing/composer.json 宣告 billing 對外暴露的介面
reporting/composer.json 明確宣告 "require": { "app/billing": "^1.0" }
CI 裡有一道檢查:任何沒有透過宣告介面的跨模組引用會直接失敗
→ AI 要修改 billing 模組內部邏輯時,
只要沒有動到對外暴露的介面,
可以合理假設「查證範圍就是 billing 這個模組本身」;
如果真的要動到對外介面,
CI 的依賴檢查會直接列出所有真正依賴這個介面的模組,
不需要自己 grep、也不怕漏找
這正是這個系列從 Day 01 開始反覆出現的模式:AI 給出「已確認這樣改沒問題」的結論,可信度取決於它查證的範圍夠不夠全面。模組邊界的價值,不是讓程式碼看起來比較整齊,而是把「查證範圍該有多大」這件事,從「AI 自己想辦法窮舉」變成「邊界工具直接告訴它答案」。
邊界模糊最陰險的地方,不是它讓大改動變得危險——大改動通常會讓人警覺、會多花時間確認。真正的風險在於:一個看起來很小的改動,因為邊界不清楚,實際影響範圍遠比表面上看到的大,而事前完全沒有訊號提醒你這件事。
延續上面的例子:如果 billing 模組裡一個看起來只是內部實作細節的方法,被 reporting 模組偷偷依賴著(沒有寫在任何介面清單裡),那麼「重構這個方法的內部邏輯」這種聽起來很安全的小改動,實際上可能悄悄改變 reporting 模組的行為——而且因為沒有邊界工具會攔下這個依賴,這個影響甚至不會在程式碼審查時被注意到,只會在某次執行時才冒出來。
到今天為止,第一部(Day 1-7)陸續講了幾個工具,各自解決不同尺度的邊界問題:
這幾個工具尺度不同,但背後是同一個命題:架構的每一層工具,做的都是同一件事——把 AI(或人)需要查證的範圍,收斂到一個可以被合理窮舉的邊界內。 沒有邊界時,任何改動理論上都可能波及整個系統;有邊界時,改動的影響範圍變成一個可以被具體回答的問題。
回想你手上系統裡某個「照理說應該獨立」的模組:如果你現在要改它內部一個看起來無關緊要的方法,你有把握列出所有真正依賴它的地方嗎?還是你其實得靠一次全專案搜尋,然後祈禱沒有漏掉?
明天正式進入第二部:沒有架構約束時,AI 實際上會怎麼寫。先從一個具體案例開始——AI 在沒有清楚模組邊界時,如何不小心讓兩個原本該獨立的模組跨界耦合在一起。