函式要嘛做一件事(改變狀態),要嘛回答一個問題(回傳資訊)——不要兩者兼做。一個既改狀態又回傳值的函式,會讓呼叫端在讀程式碼時無法確定:這行是為了副作用,還是為了那個回傳值?
🧠 Why It's a Problem
- 語意曖昧:
set("username", "bob")若同時回傳一個 boolean,讀者無從判斷——它是在設定屬性,還是在查詢屬性是否存在?名字說一件事、回傳值暗示另一件事。 - 副作用藏在查詢裡最危險:呼叫端以為只是「問一個值」,卻不知每次呼叫都改了狀態。放進迴圈、放進斷言、放進日誌,就會意外觸發副作用。
- 它其實是「只做一件事」的一個切面:命令與查詢混在一起,本質上就是一個函式做了兩件事——見 只做一件事。
- 可測性:純查詢(無副作用)可以反覆呼叫、可以在測試裡自由取值;一旦混入命令,測試就得處理狀態變更的順序與清理。
WARNING
最隱蔽的違例是「順手回傳」:addItem 加完商品後「順便」回傳總價。呼叫端很可能只想加商品,卻被迫接收一個它沒要的值;而真正想拿總價的人,又得先產生一次副作用才拿得到。
⚖️ Case Study
前 — 一個方法又改狀態又回傳
class Cart {
constructor() {
this.items = [];
}
// 混合 command 與 query:加了商品,又回傳總價
addItemAndReturnTotal(item) {
this.items.push(item);
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}
// 呼叫端:這行是為了「加商品」還是為了「拿總價」?兩者都有,意圖不明
const total = cart.addItemAndReturnTotal(book);後 — 命令歸命令,查詢歸查詢
class Cart {
constructor() {
this.items = [];
}
// Command:改變狀態,不回傳值
addItem(item) {
this.items.push(item);
}
// Query:回傳資訊,不改變狀態(可安全反覆呼叫)
totalPrice() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}
// 呼叫端意圖分明:先命令,再查詢
cart.addItem(book);
const total = cart.totalPrice();addItem 只做事、totalPrice 只答問。每一行的目的都寫在臉上,totalPrice() 也能隨意多呼叫幾次而不改變任何狀態。
TIP
判準很簡單:函式名是動詞命令(save、delete、add)就別回傳領域資料;函式名是問句(isEmpty、totalPrice、getUser)就別偷改狀態。 名字的詞性應該和它的職責一致。
🔑 Takeaways
- 函式要嘛改狀態(命令)、要嘛回傳資訊(查詢),不要兩者兼做。
- 混合會讓呼叫端讀不出意圖,也讓查詢暗藏副作用、難以測試。
- 「順手回傳」是最常見的違例——命令就讓它回 void。
- 這是 只做一件事 在「副作用 vs. 回傳值」上的具體展現。
No notes yet — jot your takeaways or Q&A here.