Day23 到 Day27,我們完成了系列的第三個階段。和前兩次一樣,先回頭看看這個階段想解決什麼、做到了哪裡。
階段二談的是如何建立 boundary,並用工具檢查它有沒有被遵守。但 boundary 即使被遵守,位置仍然可能是錯的,而且沒有任何工具會提醒我們。所以階段三問的是:boundary 該放在哪裡?又值得為未來的 change,先花多少成本?
這個問題困難的地方在於,未來的需求沒有人知道。boundary 太少,change 發生時要改很多地方;boundary 太多,平常就得為還沒發生的 change 多維護許多東西。
這五天,談的是五個需要做選擇的地方:
| 主題 | 要選擇的是 | 好處 | 代價 |
|---|---|---|---|
| Cohesion(Day23) | 什麼該放在一起 | 同一個理由的 change,只需要改一個地方 | 切得越細,一個 change 需要修改的 boundary 越多 |
| Information hiding(Day24) | 什麼該留在 boundary 裡 | decision 改變時,client 不必跟著改 | client 能使用的東西變少 |
| DRY(Day25) | 什麼該合成一份 | 同一份 knowledge,只需要改一次 | 所有 client 共用同一份,之後必須一起改 |
| OCP 與 YAGNI(Day26) | 什麼時候該開擴充點 | 新增一種做法時,不必修改舊的 code | 多一層 abstraction,要多讀、多維護 |
| Dependency 的形狀(Day27) | 兩個 boundary 之間的 dependency 該是什麼形狀 | 另一邊的 change 不會影響到這一邊 | 多一層,更難從 code 追到實際執行的地方 |
前三天談的是已經在 repo 裡的東西:什麼放在一起、什麼留在 boundary 裡、什麼合成一份。第四天談還沒出現的東西:該不該在第二種做法出現之前,就先開好擴充點。最後一天談兩個 boundary 之間的 dependency:它無法消除,能決定的只有它的形狀。
從表上可以看到,每一種設計都有代價。沒有一種結構能讓所有的 change 都變容易;而結構本身也有成本,不管預期的 change 有沒有發生,都需要有人閱讀與維護。
這五天都刻意停在比較高的層次:遇到某一種情境時,有哪些做法可以選、各自會犧牲什麼,並且盡量從根本的角度解釋為什麼。沒有任何一篇是「遇到 A,就做 B」這樣的規則。
原因是每個專案都有它的獨特性。要解決的問題、要支援的東西、需求的來源與變動的頻率,都不盡相同。同一層 abstraction,在一個專案裡經常用得到,在另一個專案裡卻只是多一層要讀的 code。直接套用別人的做法,常常只增加了成本,卻得不到好處。
這幾天出現過不少有名字的原則:SRP、DRY、OCP、YAGNI、DIP,其中三個出自 SOLID。它們都是 heuristic:從過去的經驗整理出來、在某些情況下通常有效的做法,並非任何時候都必須遵守的規定。
原則之間也會互相衝突:
如果把原則當成規定,兩個原則衝突時就沒有辦法決定。把它們當成 heuristic,衝突時要做的就是依照專案的狀況取捨。
取捨時,有三個問題在任何專案都用得上:
因此,只說「這違反了某某原則」並不夠,還要能說明:哪一種 change 會因此變得難改,以及為什麼認為它會發生。
這種判斷沒有公式,需要看過夠多的 change 才會有感覺。這五天整理的,只是比較不容易出錯的起點。
既然是取捨,就會有選錯的時候。選錯之後要做的事是 refactoring(重構):程式的行為不變,只改變它的結構。
在過去,refactoring 是成本很高的行為。工程師大多知道 codebase 越乾淨越好:可讀性高,bug 也比較少。但 refactoring 需要時間,帶來的好處卻很難衡量,畢竟做完之後,程式的行為和原本一樣,沒有多出任何 feature。對只關注結果的人來說,程式能跑就好,這些時間不如拿去開發新功能。因此結構上的問題,常常是大家都知道,卻一直沒有人去改。
有了 coding agent 之後,情況不一樣了。改名、搬移 code、拆分 module、更新每一個呼叫的地方,這些過去需要人一步一步動手的工作,現在大多可以交給 agent。前兩個階段的工具在這裡也有幫助:test 與 type checker 能確認行為沒有改變,boundary 則讓一次修改不會影響太多地方。
這帶來兩個改變。
一、不必一開始就選對。 過去修改結構的成本很高,所以會希望第一天就把設計做完整,連還沒出現的需求也先準備好。現在可以先選最簡單的做法,等根據足夠了再調整。
二、更難得的,是知道怎麼使用 coding agent。 動手修改變容易了,但要往哪個方向改、改成什麼樣子,agent 並不知道。它可以把 code 改成任何樣子,也可能越改越亂,所以需要有人告訴它方向。
要注意的是,成本變低的只有 repo 裡的 code。已經儲存的資料、已經公開給別人使用的 interface,修改起來仍然不容易。
階段三提供了一些想法,幫助我們設計好的 boundary。它帶來的好處可以這樣串起來:
好的 boundary → module 之間的耦合降低 → codebase 更好讀、更乾淨
→ agent 讀到的 context 更好 → 產出的品質更好
對 coding agent 而言,repo 就是它的 context。boundary 設計得好,它完成一次 change 需要讀的東西就比較少,讀到的也大多和任務有關。
此外,agent 傾向延續 repo 裡既有的寫法。結構好,它新增的 code 也會維持好的結構;結構亂,它也會跟著亂下去。所以這是一個循環:結構越好,agent 的產出越好,之後要調整也越容易。
要讓這個循環往好的方向發展,需要兩種 agent 沒有的資訊:
兩者合起來,就是人腦中對 repo 的想像:它現在是什麼樣子,以及它應該變成什麼樣子。
這也是階段三無法交給工具的原因。工具能檢查一條 boundary 有沒有被遵守,卻無法判斷它值不值得存在,因為這需要的正是上面兩種資訊。
因此,階段三中人的角色是:運用腦中對 repo 的想像,引導 coding agent 選出最適合這個專案的設計。
想像越清楚,來回修正的次數就越少,迭代越快,也越容易達成目標。反過來說,如果腦中沒有這樣的想像,就只能接受 agent 交出來的結果。
| 階段一:Control | 階段二:Contain | 階段三:Choose | |
|---|---|---|---|
| 無法消除的限制 | 程式可能做錯 | 系統大到無法一次理解 | 未來的需求無法預測 |
| 我們做的事 | 用可以執行的檢查確認對錯 | 把一次 change 的範圍限制在 boundary 內 | 決定 change 的成本要花在哪裡 |
| 由誰判斷 | 工具 | 工具 + 人 | 人 |
階段一與階段二,最後大多能交給工具。階段三則需要由人來判斷:coding agent 讓寫 code 與改 code 的成本都變低了,但方向仍然要有人決定。
每一次 change 都有成本,而設計,就是決定這些成本要花在哪裡。