分離客觀與主觀的程式碼
重構與提取函式庫的方法論
在 LambdaIsland 的 blog 上有一篇文章 Improve your code by separating mechanism from policy 。一直以來,我都認為它最重要,可惜,我讀了好幾遍,卻一直對該文的概念沒有夠深的理解。
為什麼覺得它重要呢?過去我還在為 Arne 工作時,他做 code review 時,會動手改個幾筆,有時候邊改他會邊說:「這個部分是 policy ,不要把它跟 mechanism 放在一起。」類似的事,發生了兩三次。總之,他也寫了文章來闡述這個概念,我想…,也許他以為我某種程度地懂了吧。可惜,我沒有。
最近我用 Claude Code 做了一個叫做 /refactor-pm 的 commands ,並且用這個 command 來批改我的程式碼。然後,神奇的事發生了,它批改時,給我的建議,還真的跟 Arne 昔日給的長得超像。透過這樣子的操作,我對這個概念得到了新的理解。
為什麼原始文章很難懂
原始的文章難懂,主要是有幾個問題:
詞彙的選擇。
缺乏例子。
不平衡。文章最重要的部分,佔用的比例太少。
詞彙選擇
作者選擇使用 Mechanism 與 Policy 兩個詞彙來描述『客觀』與『主觀』這兩種概念,因為他最初是在 UNIX 的書裡學到這兩個詞。
然而,實質上,作者在文章裡用了相當的篇幅去重新定義這兩個詞 (參考下表)。既然這樣子的話,好像其實可以不需要用這種『要想一下』的詞了,可以更乾脆地用客觀來代表 mechanism ;用主觀來代表 policy。
對讀者來說,第一次讀就要把整個對照表背下來、或是體會它,可能不太容易。記不住對照表的話,可能就只記得了 policy 與 mechanism 。於是這變成文章的要旨還是記不太住。記不太住的話,要在日後的程式設計過程中產生理解就又更難了。
當然,用了精確的詞還是有精確詞的好處,以學術的角度來看,這讓文章更顯得清晰。主觀通常指「依個人偏好而定」,確實與軟體開發裡的主觀「依當下商業決策而定」詞意有所不同。
缺乏舉例
缺乏舉例這點實在是一個硬傷。所幸,我用了該文章去做出了 /refactor-pm 這個指令之後,我就從我寫的程式碼裡,得到了不少的例子,大幅改善了我的理解。
以下以呼叫 LLM API 為例,示範兩者混雜時的樣子,以及重構後的結果。
重構前
(defn call-llm [user-message]
;; policy: model choice, temperature, max-tokens are hardcoded
(let [response (http/post “https://api.anthropic.com/v1/messages”
{:headers {”x-api-key” (System/getenv “ANTHROPIC_API_KEY”)
“content-type” “application/json”}
:body (json/encode
{:model “claude-sonnet-4-20250514”
:max_tokens 1000
:temperature 0.7
:messages [{:role “user”
:content user-message}]})})]
;; policy: error handling strategy is also baked in
(if (= 200 (:status response))
(-> response :body json/decode (get-in [”content” 0 “text”]))
(throw (ex-info “LLM call failed” {:status (:status response)})))))重構後
;; mechanism: just knows how to talk to the API
(defn call-llm [{:keys [model max-tokens temperature messages]}]
(let [response (http/post “https://api.anthropic.com/v1/messages”
{:headers {”x-api-key” (System/getenv “ANTHROPIC_API_KEY”)
“content-type” “application/json”}
:body (json/encode
{:model model
:max_tokens max-tokens
:temperature temperature
:messages messages})})]
{:status (:status response)
:body (-> response :body json/decode)}))
;; policy: opinions live here
(defn summarize-text [text]
(let [result (call-llm {:model “claude-sonnet-4-20250514”
:max-tokens 1000
:temperature 0.3
:messages [{:role “user”
:content (str “Summarize: “ text)}]})]
(if (= 200 (:status result))
(get-in result [:body “content” 0 “text”])
(throw (ex-info “Summarize failed” result)))))不平衡
文章的一大要旨是在其中一句:
The distinction here isn’t so much in what the code does, as in how it is written.
沒有這一句的話,文章可能會被這樣解讀與應用:「接下來要開發的這一塊程式碼是與 LLM API 溝通,所以,那它應該是 mechanism 。(what the code does)」
加了這一句之後,文章的解讀與應用則變成:「接下來要開發的這一塊程式是與 LLM API 溝通。那麼,在開發完之後,應該要來仔細看程式碼裡,哪些部分同時含有 mechanism 與 policy ,如果有兩者混雜的話,要把 mechanism 往下移,把 policy 往上移。」
程式碼的組織方式
程式碼很多時候圍繞著它的特性而組織的:『效能』是一種重要的特性,而為效能做極致最佳化的程式碼往往難以閱讀,所以會將其獨立組織起來 (封裝),至少讓它的介面容易理解。『副作用』也是一種關鍵的特性,只要是與外界溝通、與系統資源溝通的程式碼,都會有 I/O 、會有例外需要考慮,也因此副作用相關的程式碼往往也會與資料轉換的程式碼分開組織。
那「客觀」與「主觀」呢?它們對應的特性是『變動速度』。客觀的部分變動得很慢;主觀的部分隨時在變動。就像在原文裡所說的:
So the rate of change is very different. Good mechanism code can live on for years with hardly any changes. You may fix a bug from time to time, or make it more efficient, or perhaps extend it to support more features, but the core thing that it does stays the same.
Policy code on the other hand is really just a bunch of opinions. This is how we decided that things should currently work. Expect to be tweaking and changing this almost constantly.
總結
最近,我在思考一個問題:「用 LLM agent 來輔助軟體開發的話,該怎麼讓專案長大了之後,專案還是看得懂?」一直以來,我心裡的答案之一是:「邊開發要邊設法將函式庫 (library) 抽取出來。」那要怎麼讓 LLM 也能觀察出 pattern ,來協助我們找出適合提取 library 的程式碼呢?
客觀的部分變動慢、邊界清晰,正是適合抽取成 library 的特徵。換句話說,持續重構讓 mechanism 浮現的過程,其實就是在為未來抽取 library 鋪路,也讓 LLM 更容易從穩定的結構裡辨識出可複用的 pattern。
前人的文章已經給了答案,邊開發就要邊不斷地重構,才能讓客觀的部分逐步浮現。


