從今天開始,用幾個具體案例,把前面 23 天講過的原則串起來看——不是重複講一次「架構邊界很重要」,而是看這些原則在真實情境裡怎麼一起發揮作用,或者,怎麼在缺席的時候一起失效。
「這次修的 bug 明明測試都過了,怎麼上線後另一個功能反而壞了?」這是我在推動架構邊界這件事之前,最常聽到的抱怨。今天要講的案例,正是這句話背後最典型的成因:AI 修好的那個地方確實沒問題,出事的是它根本不知道自己該顧到的另一個地方。
假設系統裡有一段「計算訂單折扣」的邏輯,原本寫在一個共用的工具函式裡,沒有明確的模組歸屬——訂單模組在結帳流程呼叫它,行銷活動模組在計算活動預算時也呼叫它。兩個模組之間沒有任何文件或程式碼結構標示這層共用關係,純粹是「剛好都 import 了同一個檔案」。
有一天,訂單模組回報一個 bug:某些訂單的折扣金額多算了一分錢,原因是四捨五入的時機不對,應該先加總再四捨五入,卻寫成先四捨五入再加總。AI 被要求修這個 bug,讀了訂單模組的測試、讀了這段函式的呼叫端,判斷「訂單模組的呼叫情境下,先加總再四捨五入才是對的」,於是調整了四捨五入的順序,訂單模組的測試全部變綠。
問題是,行銷活動模組依賴的正是「先四捨五入再加總」這個舊行為——活動預算的計算邏輯裡,刻意要求每筆訂單的折扣要先四捨五入到分位,才能跟財務系統的帳務邏輯對齊。AI 改完之後,訂單模組的 bug 修好了,行銷活動模組的預算計算卻悄悄壞掉了,而且沒有任何測試抓到,因為 AI 根本不知道要去執行行銷活動模組的測試。
AI 不是沒有查證,是它查證的範圍剛好跟這段程式碼的真實依賴範圍不一致。 它跑了訂單模組的測試、也跑了這段函式本身的單元測試,這些測試全部通過——但這些測試的範圍是「AI 找得到、且合理猜測會受影響」的地方,不是「這段程式碼實際上被所有依賴者需要的樣子」。這正是這個系列反覆講的模式在架構層面的樣貌:AI 的查證範圍,跟這段程式碼真正的影響半徑之間,隔著一層邊界模糊帶來的落差。
如果這段折扣計算邏輯有清楚的模組歸屬——例如它明確屬於某個「計價」模組,行銷活動模組只能透過一個穩定的公開介面呼叫它,而不是直接 import 同一支檔案——這個依賴關係就會顯性存在於程式碼結構裡,而不是只存在於「兩個模組剛好都 import 了同一個檔案」這種容易被忽略的巧合中。AI 改動一個被明確引用的公開介面時,天然會去查「還有誰在呼叫這個介面」;但改動一個沒有邊界、大家都可以隨意 import 的共用檔案,AI 沒有結構性的線索提醒它「這裡不只一個依賴者」。
用一組對照來看這個差異:
❌ 邊界模糊,依賴關係隱性存在:
// 訂單模組
import { calculateDiscount } from '../../shared/pricing-utils';
// 行銷活動模組
import { calculateDiscount } from '../../shared/pricing-utils';
→ 兩個模組各自 import 同一支檔案,沒有任何介面契約、
沒有任何機制提示「改這裡要注意誰在用」,
AI 只能靠自己搜尋,而搜尋範圍取決於它當下判斷「這值得查多廣」
✅ 邊界清楚,依賴關係顯性存在:
// pricing 模組對外的公開介面
export interface PricingService {
calculateDiscount(order: Order): Money;
}
// 訂單模組、行銷活動模組都只依賴這個介面,
// 修改 PricingService 的實作時,
// 型別系統/IDE/CI 都能列出所有呼叫端,
// 不需要 AI 自己猜「還有誰在用」
架構邊界在這裡發揮的作用,不是防止 bug 本身,而是把「這段程式碼的影響範圍」從一件需要 AI 自己搜尋、猜測的事,變成一件結構本身就能回答的事。 呼應這個系列的主題句:AI 查證得再仔細,也只能查到它找得到的地方;架構邊界的價值,是讓「找得到的地方」跟「真正需要查的地方」盡量重合。
回想你手上系統裡有沒有類似「兩個模組剛好都 import 了同一支共用檔案」的情況——如果讓 AI 去改這支檔案,你有把握它會想到要去檢查另一個模組嗎?
明天是相反方向的案例:當介面邊界劃得夠清楚時,一次原本看起來很有風險的大範圍重構,怎麼因為邊界本身提供的保護,變成一件可控的事。