如果你的功能被大量使用,就別指望使用者會遵守你的限制——因為所有情境都有可能發生。
出處:Software Engineering at Google 中提到的 Hyrum’s Law
這篇文章是我閱讀 What I learned from the book Software Engineering at Google 整理的筆記
什麼是 Hyrum’s Law?
Hyrum’s Law(海倫姆定律)指出:當 API 的使用者數量足夠多時,你在合約中承諾什麼並不重要,系統所有可觀察的行為都會被某些人依賴。
這意味著在設計與維護系統時,不能只看規格文件,還必須考慮使用者實際上怎麼使用你的系統。
為什麼這條定律會成立?
使用者不會去讀完整的 API 文件,他們是透過「觀察行為」來理解系統的。當某個未定義的行為在多次執行中表現穩定時,使用者就會將它視為隱式契約(implicit contract),並在自己的程式碼中依賴它。使用的人越多,這種隱式契約被建立的可能性就越高。
案例一:Java HashMap 的迭代順序
HashMap 文件中明確提到不保證順序性,但就是有工程師用 containsElementInOrder() 對輸出做斷言,甚至有人把迭代順序當作隨機數產生器。
以 TypeScript 來模擬這個情境:
// 模擬 Java HashMap 的行為(不保證順序)
function unstableHash(key: string): number {
// 不同版本的 hash 演算法可能給出不同結果
return key.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0) % 4
}
// 把 hash 迭代順序當「隨機數產生器」
function ghettoRandom(seed: string): number {
const map = new Map<number, number>();
for (let i = 0; i < 100; i++) {
map.set(unstableHash(seed + i), i);
}
return [...map.keys()][0]; // ← 拿第一個 key 當隨機值
}這段程式碼的問題在於:我們根本不能保證結果一定會是不同的亂數,hash 碰撞會讓 key 重複覆蓋,但就是有人這樣寫了。
當 Google 升級 Java 版本後,hash 演算法改變導致順序不同,許多程式在測試中大量失敗。
Google 的解決方式是採用防禦性隨機化——每次執行都打亂順序,讓人從根本上無法依賴迭代順序。Python 跟 Go 後來也獨立採用了相同的做法。
案例二:Recall.ai S3 URL 冒號事件(2024)
起因
Recall.ai 發布了一個更新,在 S3 URL 路徑中加入了 :(冒號)。URL 編碼的冒號在 URL 中是完全合法的字元,而且客戶根本不需要解析 URL,只是用 GET 請求從 S3 下載影片而已。他們認為這不可能會有問題。
發生問題
部署後立刻收到影片下載失敗的回報。調查後發現只有一小部分客戶受到影響,但這些客戶 100% 的請求都會失敗。
根本原因
受影響的客戶都使用 aiohttp 這個 HTTP client,而 aiohttp 依賴 yarl 這個 library 做 URL 解析。yarl 預設會對 URL 進行規範化(normalization),自動把「安全字元」做 URL 解碼。這導致實際發出的請求路徑跟 S3 簽章的路徑不一致,S3 判定簽章無效,回傳 403 拒絕請求。
解法
Recall.ai 嘗試繞過 yarl 的行為但失敗了,最後因為受影響的客戶數量少,選擇通知客戶加上一行程式碼關閉 yarl 的規範化:
python
yarl.URL(url, encoded=True)這跟 Hyrum’s Law 有什麼關係?
Recall.ai 的 URL 在這次更新之前「剛好」從來沒包含過需要編碼的安全字元,所以 yarl 的規範化行為從來沒被觸發過。客戶端——甚至客戶自己都不知道的第三層依賴——已經隱式地依賴了「URL 不會被修改」這個未承諾的行為。一旦 URL 格式稍有變化,這個隱式契約就被打破了。
所以我們該怎麼做?
既然所有情境都有可能發生,指責使用者不閱讀文件並不能解決問題。真正有效的做法是從設計層面讓使用者無法依賴未承諾的行為,就像 Google 的防禦性隨機化一樣。
當你的功能被廣泛使用時,任何變更都應該假設「所有可觀察的行為都已經被某些人依賴了」,用設計來防範,而不是用文件來約束使用者。