軟體設計

Single Source of Truth,把相同資料放在兩處就一定會不一致

許多在部署前才發現的錯誤都有固定模式:前端把折扣率算成10%,後端卻算成15%;文件寫著某欄位必填,但實際API中卻已消失。兩邊的程式碼本身都沒問題。問題在於相同資訊出現在兩處…

閱讀 4 分鐘
Single Source of Truth,把相同資料放在兩處就一定會不一致 封面圖

許多在部署前才發現的錯誤都有固定模式:前端把折扣率算成10%,後端卻算成15%;文件寫著某欄位必填,但實際API中卻已消失。兩邊的程式碼本身都沒問題。問題只是相同資訊存在於兩處,而只有一邊被修改了。

從結構上避免這類事故的原則,就是Single Source of Truth,簡稱SSOT。名稱聽起來很宏大,但內容只有一句話。每一個資訊片段都必須只有一個權威的原始來源。

重點總結如下。

  1. SSOT是「資訊只有一個原始來源」的原則,不代表儲存位置只能有一個
  2. 重複的資訊一定會不一致。真正的問題不是不一致發生的時刻,而是沒有人知道哪一份才正確的時刻
  3. 需要複製時,請把它做成「衍生物」。不要手動寫兩次,而是從原始來源自動產生
  4. 快取與複本不違反SSOT。只要清楚標示原始來源在哪裡,以及更新方向即可

為什麼重複一定會造成不一致

一旦把資料放在兩處,維持兩者一致的責任就落到人身上,因為工具和編譯器不知道這個約定。每次修改都必須記得「這個值還在哪裡?」只要忘記一次,兩個值就會走上不同的路。

更棘手的是不一致發生之後。當程式碼兩處的常數出現不同值時,只看程式碼無法知道哪一個正確。接著就得翻git歷史、尋找當時的負責人、查閱規劃文件,展開一場考古。SSOT崩潰的真正成本不是修正錯誤,而是判定「哪一份才是真相」所花的時間。

先來看幾個常見案例。

  • 常數重複:最大上傳大小10MB分別硬編碼在前端驗證、後端驗證與錯誤訊息字串中
  • 驗證邏輯重複:用不同的正規表示式,在用戶端與伺服器執行電子郵件格式檢查
  • 文件與程式碼:API規格文件一份、實際實作一份。幾個月後,文件就變成小說
  • 資料庫與快取:更新原始來源後忘了讓快取失效,舊資料持續被提供
  • 設計與程式碼:設計稿的色彩值與硬編碼在App中的色彩值有細微差異

形式雖然不同,結構卻一樣:原始來源有兩份,而同步依賴人工處理。

比較沒有SSOT時靠手動同步導致數值不一致,以及從共用原始來源衍生資料的結構圖
這是交給人類記憶同步,與從原始來源衍生資料的結構差異

原則不是「禁止複製」,而是「衍生」

誤解SSOT時,很容易把它理解成「資料只能儲存在一處」。如此一來,快取、讀取複本和建置產物看起來全都違反原則。實際原則不同。可以存在於多處,但原始來源只能有一個,其餘都必須從原始來源衍生。

建立衍生物的代表方法就是程式碼產生。

  • 從結構描述產生型別:從OpenAPI規格產生用戶端型別與伺服器Stub後,規格就是原始來源,文件與程式碼便沒有不一致的機會
  • 設計權杖:把色彩與排版定義在一個權杖檔案中,再分別產生iOS、Android與Web程式碼
  • 共用常數模組:讓前端與後端都從同一個套件import常數,從根本阻止硬編碼複製
  • 從程式碼擷取文件:從註解與型別產生API文件,文件就會一直跟著程式碼更新

共同點是把「人要寫兩次」的地方,改成「機器產生一次」的地方。同步責任從人的記憶移到建置管線;一旦不一致,CI會先發現。

快取與複本也能用同一框架整理。只要宣告原始來源的位置,更新只沿著原始來源→複本單向流動,並定義複本可能過期的時間(TTL、失效策略),就遵守了SSOT。違反發生的時機不是「存在複本」時,而是「開始直接修改複本」時。

同一原則也適用於組織

SSOT不只是程式碼的議題。在微服務設計中決定「哪個服務擁有這份資料」,以及從散落在公司Wiki、Notion、Slack的政策文件中指定「正式版本」,本質上都是同一個問題。

文件尤其比程式碼更容易不一致,因為沒有編譯器也沒有測試。因此,組織層級的SSOT必須先有共識,再談工具。例如規定「入職流程的原始來源只有Wiki這一頁,其他地方只放連結」。以連結取代複製,就是文件世界的衍生。

從結構描述原始來源自動產生型別、文件與設計權杖的程式碼產生管線插圖
把人要寫兩次的地方,改成機器產生一次的地方

實務檢查清單

整理成明天就能套用的判斷基準如下。

  1. 第二次輸入相同值的瞬間就是訊號:如果正在複製常數、驗證規則或設定值,先檢查能否移到共用模組或改用產生方式
  2. 宣告原始來源:快取、複製與摘要本身都不是問題。確認程式碼和文件是否明確寫著「原始來源在這裡」,以及更新是否為單向
  3. 讓工具而不是人找出不一致:把結構描述驗證、契約測試與產生程式碼的diff檢查加入CI,讓同步失敗在合併前浮現
  4. 留意完美主義:為消除所有重複而建立過度抽象,本身也會產生成本。應先為經常變動,或不一致時代價高昂的資訊建立SSOT

這和重構中的DRY(Don’t Repeat Yourself)原則同源。DRY針對程式邏輯的重複,SSOT則進一步涵蓋資料與知識的重複,可視為更高層次的原則。下次出現「這個值那邊也有,要一起修改嗎?」時,那就是建立SSOT的地方。