CLAUDE.md裡有一行「分層是api → services → repositories」。我照著跑完一個工作單元,產出的是四個目錄——而規則沒有攔住我,最後是規則被改掉。
昨天把開發流程改成測試先寫,也真的跑完了一輪,測試可以幫我確認一件事:**功能有沒有照規格運作。**例如「確認人按下確認後,版本的確認狀態要更新」,測試可以直接驗證結果對不對。
但假設有人把查資料、業務判斷全部寫進 api/,功能沒壞,測試也沒紅,但程式已經開始偏離架構,這就是昨天測試沒有處理到的另一種問題:
測試可以驗證「事情有沒有做對」,但不一定知道「這件事是不是放在對的地方」。
那架構規則要靠什麼守?
專案建起來的時候,/init 掃過程式碼,把後端分成三層:
**Layering intent** (backend): `api/` routes → `services/` business logic
→ `repositories/` data access.
實作完後回頭看產出的目錄:
app/api/ app/services/ app/repositories/
app/models/ app/schemas/ ← 這個不在那條鏈上
schemas/schemas/ 它裝的是對外的請求與回應模型,不屬於 api/、services/、repositories/ 這三層。
因此 claude 實作時自己決定產出四層,CLAUDE.md 也被改成四層。
**`app/schemas/` is a fourth directory, deliberately outside that chain.**
這段是跟實作同一筆 commit 進去的——不是事後檢討才補的,是實作當下順手改的。
services/errors.py產出的內容 services/errors.py 裡的例外類別帶著 HTTP status code:
class ConflictError(DomainError):
status_code = 409
HTTP 狀態碼是傳輸層的概念,出現在 service 層算是跨層。純粹一點的做法是 service 只丟領域錯誤,由 api/ 那層決定對應哪個碼。我沒那樣做——換來的是所有錯誤在 main.py 一個地方渲染成同一個形狀,前端只需要處理一種錯誤結構。
兩個例外,差別只在有沒有被記錄
| 是什麼 | 理由寫在哪 | 什麼時候寫的 | |
|---|---|---|---|
schemas/ 目錄 |
一個不在鏈上的目錄 | CLAUDE.md |
事後,跟實作同一筆 commit |
errors.py 帶狀態碼 |
一個跨層的欄位 | 檔案開頭的 docstring | 當下 |
抓到例外的是plan.md 裡的檔案清單。
規劃時列了 36 個檔案,實作完變成 41 個。因為清單在,多出來的每一個都得解釋:
| 檔案 | 為什麼原本沒列 |
|---|---|
schemas/user.py |
UserRead 同時被三份 schema 用到。塞進其中任何一份,都會讓另外兩份反向 import |
repositories/user_repository.py |
原本想把 user 查詢塞進會議的 repository。但每個請求都要查 user,掛在會議底下不合理 |
services/errors.py |
契約裡的錯誤碼約定需要一個載體。塞進狀態機那個檔案,會讓它同時是狀態機和錯誤型別定義檔 |
services/user_service.py |
分層是 api → services → repositories。少了它,api/ 就得直接呼叫 repository——在自己的規則上開一個例外 |
tests/test_concurrency.py |
並行測試需要兩條真實連線,不能用其他測試那套交易回滾的隔離方式 |
這五行理由記錄的是規劃時想不到、實作當下才浮現的結構壓力。
規則是文字,清單則是可以逐項核對的東西。
清單要人去對。
有些規則可以變成機器問得出來的問題,像是「分層」規則拆開來,其實是三句可以檢查的話:
| 規則 | 檢查方式 | 實測 |
|---|---|---|
| router 不做判斷 | api/ 裡有幾個 if |
0 |
| repository 不做判斷 | repositories/ 裡有幾個 raise |
0 |
| service 不碰傳輸層 | services/ 有沒有 from fastapi |
沒有 |
這跟前面幾天是同一件事:能被落實的規則,是那些寫得成檢查的。只是這次檢查的不是業務規則,是結構。
CLAUDE.md 是 context,不是強制配置。它會被讀到、大部分時候會被遵守,但它沒有「拒絕」這個動作。以為寫進去就安全了,是把提示當成了閘門。
一句話的規則 → 要讀完整個目錄才知道有沒有違反
一份檔案清單 → 逐項核對,多出來的要解釋
一個 grep 得出的數字 → 30 秒回答得出來
越往下越強,但也越窄——status_code = 409 就是證據:grep 回報乾淨,跨層的東西還是在那裡。
文字負責描述意圖,清單負責核對變化,機器檢查負責守住可以形式化的邊界。
所以三種都需要,不是挑一個。
這一輪有兩個例外:多開 schemas/ 目錄、errors.py 帶狀態碼。兩個我都認為是對的決定。
差別在於後者的理由寫在程式碼裡、當下就寫了;前者是事後才補進規範的。而事後補的那種,下一個人只會看到一個「本來就是四層」的規則,看不到它曾經是三層、也看不到誰決定要多一層。
明天:資料庫與 Alembic。