核心原則

同一個值的權威來源只能有一個位置。 「位置」可以是 CSS selector、可以是定義機制、可以是函式 — 重點是讀者能明確指認「這個值的真相在哪」。多個來源會在時間維度上分歧、漏改、debug 時不知道哪個生效。SSoT 不是潔癖、是維護性的物理基礎。


為什麼多源會 drift

時間維度的失敗模式

寫程式的當下、多源沒問題 — 兩個值剛寫進去都是 64px、看起來一致。問題出在後續:

時間點一源情境多源情境
第 1 次寫寫一處寫多處(手動同步)
第 2 個月、需求變動要改值改一處、所有引用點自動跟上改多處、可能漏掉
第 6 個月、新人接手看一處就知道不知道哪個是「真的」
第 1 年、不同人改不同源多源開始分歧、產生隱性 bug

多源的隱形成本在時間維度累積。寫的當下看不出問題、是因為當下還沒分歧。

寫程式時的觀察盲點

寫多源的人通常不是不知道 SSoT 原則、是沒意識到自己在寫多源。常見盲點:

盲點看起來像實際上是
「複製過來改一下、改完就同步了」兩處數值同步多源的開始 — 之後改哪一處不一定
「這個常數兩個檔案都會用、各自宣告比較清楚」各檔自包含多源 — 改的時候要 grep 找全
「寫死 + 量測雙保險、哪個對用哪個」防禦設計多源 — 不知道哪個生效
「先寫一個、之後重構抽出 token」漸進式多源固化 — 通常沒有後續重構

辨識自己寫多源、需要主動的 sanity check:「這個值的真相在哪?只有一個答案嗎?」


Fact vs Derivation 的區分

SSoT 適用對象是、但不是所有「值」都該 SSoT — 區分兩種:

類型定義SSoT 規則
Fact(事實值)設計決定、不能從別處算出只能在一處宣告
Derivation(導出值)從 fact 計算得出完全用 fact 計算、不重複宣告 fact

例子(CSS)

 1/* Fact — 設計決定 */
 2:root {
 3  --search-title-h: 64px;        /* H1 高度是設計選擇 */
 4  --search-form-h: 68px;         /* form 高度是設計選擇 */
 5  --search-gap: 20px;            /* 間距是設計選擇 */
 6}
 7
 8/* Derivation — 從 fact 計算 */
 9.scope-position {
10  top: calc(var(--search-title-h) + var(--search-form-h) + var(--search-gap));
11  /* 不該寫 152px、那是 derivation 重複了 facts */
12}

把 derivation 寫成具體值(152px)= 把 fact 在 derivation 裡再宣告一次 = 多源。

區分 fact / derivation 的判讀問題

寫一個值時自問:「這個值是設計選擇、還是從別的值算出來的?」

  • 設計選擇 → fact、找住址定義
  • 從別的值算出來 → derivation、用 calc / function 表達、不寫具體數字

混淆兩者會固化多源 — 把 derivation 寫成數字、未來 fact 改了 derivation 不會跟著改。


三類 SSoT 違反

跨 #3 / #26 / #27 的三類具體表現:

違反 1:定義位置散落(住址多源)

同一個值在多個 selector 重複定義:

1/* 散在三處 */
2:root           { --search-title-h: 64px; }
3body.page-search { --search-form-h: 68px; }
4.search-shell   { --search-gap: 20px; }

不是錯、但維護者要 grep 才知道哪些值在哪、改的時候容易漏掉某處。

解法:定義集中在「跟使用範圍最匹配的最高層 selector」、其他地方只 var() 引用。

對應實作:#26 CSS 變數定義位置統一

違反 2:來源機制混搭(機制多源)

同一個值有多種「取得方式」並存:

1// 對齊基準上四個值、其中三個寫死、一個量測
2--search-title-h: 64px;            // 寫死
3--search-form-h: 68px;             // 寫死
4--search-gap: 20px;                // 寫死
5--search-scope-h: ResizeObserver;  // runtime 量測寫回

寫死值依賴的渲染條件變了(字型、theme、scale)— 量測值會跟著變、寫死值不會、兩者錯位。

解法:選一邊走到底。內容靜態 → 全寫死;內容動態 → 全量測。混搭就是多源。

對應實作:#27 runtime 量測模式統一

違反 3:對齊基準的真相分散(語意多源)

對齊問題本質是「方程組」 — 每個參與對齊的值都是基準的一個分量。任何一個分量值不確定、整個基準都靠不住。

1問題:filter padding-top 對不準 H1 + form 下緣
2分量:H1 (64px) + form (68px) + gap (20px) = 152px
3若任一分量是「我估的」、整個 152px 就不可信

解法:每個分量都要有明確的「真相位置」 — 寫死的 token 或 ResizeObserver 量測寫回變數、二選一。沒有「估算」這個選項。

對應實作:#3 視覺對齊用單一真實來源


設計工具

1. 定義集中、引用分散

1body.page-search {
2  /* 集中定義 */
3  --search-title-h: 64px;
4  --search-form-h: 68px;
5}
6.search-shell .pagefind-ui__drawer {
7  /* 分散引用 */
8  margin-top: calc(var(--search-title-h) + var(--search-form-h));
9}

「集中定義」= fact 一個住址;「分散引用」= derivation 不重新宣告 fact。

2. 命名前綴標明範圍

1--token-*           /* 全站 design token */
2--page-search-*     /* 搜尋頁專用 */
3--pagefind-ui-*     /* 組件 hook */

前綴讓維護者一眼看出值的「歸屬」 — 改的時候知道影響範圍、不會誤改別處。

3. JS 寫入跟 CSS 定義同 selector

1body.page-search {
2  --search-scope-h: 60px;  /* fallback */
3}
1document.body.style.setProperty('--search-scope-h', h + 'px');
2// 寫到 body.style、跟 CSS 定義同 selector、cascade 一致

JS 寫入位置跟 CSS fallback 在同一 selector — 兩套機制保持一致來源。

4. 用 calc 表達 derivation、不寫具體數字

1/* 好 — derivation 用 calc */
2.scope-position { top: calc(var(--a) + var(--b)); }
3
4/* 較差 — derivation 寫具體數字 */
5.scope-position { top: 152px; }  /* 152 是 a + b 算出來的、現在固化在這裡 */

calc 把 derivation 顯式表達 — 未來 fact 改了、derivation 自動跟上。


不該套用 SSoT 的情境

跟其他原則一樣、SSoT 也有適用邊界:

情境為什麼可以多源
跨系統的紀錄(DB + cache)多源是效能 / 可用性的設計、有顯式同步機制
跨服務的 reference data微服務各自存一份是常態、有 eventual consistency
國際化字串(en + zh-TW)各語言版本是「不同 fact」、不是同一 fact 的多源
開發 / production 環境的設定各環境是「不同 fact」、不是同源 drift

核心判準:多源是不是「同一 fact 的多份拷貝」?是 → 違反 SSoT;不是 → 各自是獨立 fact、不違反。


跟其他抽象原則的關係

#43 最小必要範圍 處理「範圍越窄越穩定」、本原則處理「值的住址越唯一越穩定」。兩者方向不同、目的相同 — 都是讓行為可預測:

  • 最小必要範圍:縮影響範圍、避免誤命中
  • SSoT:縮值的來源數、避免分歧

兩者經常同時出現:縮 selector 範圍時、selector 本身的定義也該 SSoT(避免散在多處)。


對應的實作篇

每篇示範這個原則在不同議題的應用:

SSoT 違反類型修正方向
#3 視覺對齊用單一真實來源對齊基準分量值來源不明每分量都要有明確住址
#26 CSS 變數定義位置統一變數定義散多 selector集中在使用範圍的最高層
#27 runtime 量測模式統一寫死跟量測混搭選一邊走到底

讀的時候從本篇出發、依議題挑實作篇。


判讀徵兆

訊號自問回應
「改一個 token 要 grep 找定義位置」定義是否散落?是 → 集中到一處
「不知道哪個值生效」來源是否多源?是 → 找出多源、保留一個權威來源
「我估的值跟實際差 2px」該值是否該量測或從 fact 算?是 → 補真相位置、不用估算
「兩處數值看起來該一致、實際分歧了」是否多源 drift?是 → 抽出共同 fact、各處引用
「複製這個值過來改一下」寫多源前自問?警訊 → 抽 token、不要複製

核心原則:SSoT 守的是「未來改值時、知道改哪裡」 — 寫的時候多想一秒、未來改的時候少痛半天。多源是時間維度的隱形成本、寫程式當下看不出來、是因為時間還沒到。

延伸到 stream 操作:#64 Feature 操作要跟 Source 同層合成 是本原則在 stream 領域的應用 — 在下游做 filter / sort = 等於建了個第二定義(subset 上的「filter 結果」)跟 stream 全集競爭、是另一種形式的 SSoT 違反。多源便利(就地寫個值)、單源對齊(找 fact 位置)— 這個反相關的更高層原則見 #67 寫作便利度跟意圖對齊反相關

延伸到 UI state:#70 URL 是 stateful UI 的儲存層 是本原則在「可分享 state」的應用 — URL 是該類 state 的 SSOT、不寫 URL = state 多源(in-memory + 使用者期望的 URL 但實際不存在)。