在階段一 Control 中,我們介紹的工具,大多是在用規則限制一次修改。Git 讓我們看見改了什麼;formatter、linter、type checker、test 與 CI,則要求每次修改都符合專案已經定義好的 constraints。
但即使每次修改都通過了檢查,還有一個問題沒有解決:為了改這一小段程式,我們到底需要知道多少其他東西?
這是階段二要處理的問題,而它的第一個概念,就是 boundary。
Boundary,中文可以翻成「邊界」。
最簡單的理解方式,就是把一段程式分成裡與外兩邊:
站在外面使用它的一方,我們稱為 client;在裡面負責實作的一方,稱為 implementer。
舉一個簡單的例子:
get_today("Asia/Taipei") # -> "2026/09/27"
對 client 來說,只需要知道三件事:傳入一個時區,會得到那個時區的今天日期,格式是 YYYY/MM/DD。
但在 function 裡面,implementer 要處理的事情多得多:從系統時鐘取得目前的 UTC 時間,套用時區,處理跨日,再轉換成年月日並格式化。這些細節,對 client 來說都不重要。
因此,boundary 做的事,是把兩種資訊分開:使用它需要知道的資訊,以及實作它需要知道的資訊。
它並不只是把程式碼包起來而已。真正的重點在於,boundary 外的人可以忽略裡面的資訊,仍然正確地使用它。
Boundary 並不是某種程式語言提供的 feature。只要有「使用它」與「實作它」兩種角色,就有 boundary。
以一個記帳 app 為例,使用者只看得到兩個 API:
POST /entries 記下一筆帳目
GET /entries 查詢已記錄的帳目
使用者只在意能不能記帳、能不能查帳。至於帳目存在 SQLite、PostgreSQL,還是另一個 cloud service,都是 boundary 裡的事情。
這樣的 boundary,在軟體裡是一層套著一層的。我們可以用一間公司來想像:
公司
├── 業務部
│ ├── 北區組
│ └── 南區組
├── 財務部
│ ├── 會計組
│ └── 出納組
└── 研發部
公司對外有一個窗口。客戶有需求時,只需要透過窗口提出,不必知道公司裡面有哪些部門,也不必知道事情最後交給了誰。
公司裡面分成業務、財務、研發等部門,每個部門同樣有自己的窗口。業務部需要財務部協助時,只需要找財務部的窗口,不必知道財務部內部怎麼分工;財務部重新調整內部的分組,也不會影響到業務部。
部門底下再分成組,組裡再分到個人。每一層都有多個單位,每個單位也都只透過自己的窗口與外界往來。
這也代表 client 與 implementer 是相對的角色。部門主管對公司高層而言,是負責把事情做完的 implementer;但要完成工作,他得把任務交給底下的組,這時他又成了 client。同一個單位,對外是 implementer,對內則是 client。
軟體也是如此。一個 application 底下有多個 module,module 裡有 object 與 function,function 又會呼叫其他 function。每一層都有多個 boundary,而修改其中一個時,我們只需要理解它本身,以及它與其他 boundary 之間的窗口。
Boundary 的價值,可以從邊界的兩側來看。對 client 而言,它減少了需要知道的資訊;對 implementer 而言,它讓裡面可以自由修改。而這兩件事要同時成立,中間這條線本身也必須守住,決定哪些資訊可以跨過邊界。
假設有一個負責儲存資料的 storage:
storage.save(entry)
對 client 來說,只需要知道 save() 可以把資料存起來。
但 implementer 可能要處理 SQL、transaction、connection pool、retry、schema migration,甚至不同 database 之間的差異。
如果每次呼叫 save(),client 都必須先理解這些細節,那這層 boundary 幾乎就沒有意義。
Boundary 把這些複雜度留在裡面,因此:
client 可以用更少的資訊完成它要做的事。
Boundary 除了隱藏複雜度,也在限制外面能依賴什麼。
原本,我們希望外部只透過:
storage.save(entry)
存取資料。
但如果為了方便,把底層 database 直接暴露出去:
storage.db.execute(...)
其他地方很快就可能開始直接讀寫 table、依賴 column 名稱,甚至依賴特定 database 的語法。
這時候,database schema 就不再只是 storage 裡的 implementation detail,而是整個系統都知道、也都依賴的事情。
未來只要 schema 改掉,或 database 想換成別的實作,原本不相關的地方也可能一起壞掉。
換句話說,boundary 限制的不只是裡面有什麼,而是:
Boundary 決定外面的人可以知道哪些東西,哪些細節應該被藏在裡面。
反過來說,如果 client 只透過:
python storage.save(entry) storage.list()
使用 storage,那 implementer 就可以改變裡面的做法。
一開始資料量很少時,可以使用 SQLite;之後資料增加,可以換成 PostgreSQL。只要 save() 與 list() 的使用方式沒有改變,client 的程式碼一行都不用動。
但不是每一種 change 都留得住。如果要支援多裝置同步,把資料改成經由 remote API 儲存,save() 就可能因為網路而失敗,或變得比較慢。這些改變會穿過 boundary,client 也得跟著處理。
因此:
implementer 可以修改裡面,而大部分的 change 不會擴散到外面。
Coding agent 在完成一項任務時,同時扮演著這兩種角色。
一方面,它是 client。要完成任務,agent 得使用別人寫好的 function 與 module,例如為記帳 app 加上依分類篩選時,就得透過 storage 取得帳目。
好的 boundary,讓它只看外面就知道該怎麼用,不必把別人的實作一併讀進 context。
另一方面,它也是 implementer。agent 寫出的程式,會成為其他人,以及下一個 agent session 要使用的外面。因此它也得負起 implementer 的責任:把細節留在裡面,不讓它外溢;已經對外提供的使用方式,也不能隨意改變。
一次任務裡,agent 往往同時站在許多條邊界的兩側。這一點對人來說也一樣,只是 agent 每個 session 都從零開始,無法依賴過去累積的記憶,更需要 boundary 本身就把裡與外分清楚。
因此,好的 boundary 讓 agent 身為 client 時能少讀一點,身為 implementer 時也知道該守住什麼。