功能開關
問題
有一組功能已經寫完、測過、能跑,但現在還不該給使用者看到:
- 相關流程還沒整合完,先出現只會引起誤用
- 要讓使用者先專注驗收另一塊,多餘的入口會分散注意力
- 資料還沒備齊,畫面打開會是一片空的
三種常見做法,三種不同的痛:
| 做法 | 問題 |
|---|---|
| 把程式碼刪掉,之後再寫回來 | 「之後」寫回來的是另一個東西。測試、示範資料、邊界處理全部要重來 |
| 開一條長期分支不合併 | 分支活得越久,合併那天越痛。而且分支上的程式碼不會被 CI 與其他人的改動照顧到 |
| 靠環境區分(開發環境開、正式環境關) | 正式環境跑的是沒有人真正用過的那個組合。你驗收的版本不等於你發佈的版本 |
做法:純布林開關,只隱藏入口
export const FEATURES = {
/** 存量清查(清查作業畫面 + 設定分頁 + 結算頁的參考數量) */
quantityCheck: false,
/** 任務追蹤(時數拆分/成本/完成預估 + 工作台的超額警示) */
taskTracking: false,
} as const;
1. 只隱藏入口,不刪程式碼、不刪資料、不動計算
被關掉的功能,它的一切都留著:
| 留著什麼 | 為什麼 |
|---|---|
| 狀態切片、示範資料 | 重啟時不必重建資料模型 |
| 純函數(計算、彙總) | 它們有測試,測試繼續跑,不會腐化 |
| 元件與畫面 | Storybook 仍可預覽,設計仍可迭代 |
| 資料庫欄位與既有資料 | 關功能不是刪資料。刪了就回不去了 |
唯一被拿掉的是使用者走得到的路。 這讓開關的成本停留在「幾行判斷」,而不是「一次重構」。
2. 純布林,與環境脫鉤
開關的值是寫死的 true / false,所有環境一致。
這一點違反很多人的直覺(「不是應該開發環境打開嗎?」),但理由很硬:
為什麼不依環境決定
開關一旦與環境綁定,你驗收的畫面就不是使用者會看到的畫面。 開發環境有 12 個側邊欄項目、正式環境有 9 個—— 那是兩套資訊架構,而其中一套從來沒有被完整走過。
如果目的是「開發時要能看到」,正確做法是改那一行布林值(改完自己看,不提交), 而不是讓程式自己依環境決定。差別在於:前者不會被誤發佈,後者一定會。
3. 把 gating 點列成清單
這是實務上唯一真正會出錯的地方:功能的入口不只有側邊欄那一個。
| gating 點 | 漏掉的後果 |
|---|---|
| 側邊欄項目 | ——(這個不會漏) |
| 路由本身 | 使用者用舊書籤直接開網址,進到一個半殘畫面 |
| 未知路徑的 fallback | 關掉的畫面剛好是預設首頁 → 白屏 |
| 其他畫面的跨功能引用(工作台待辦、彙總報表的某一區、結算畫面的參考數值) | 到處出現空區塊,或是引用不存在的資料而炸掉 |
| 設定頁的分頁 | 使用者設定了一個不存在的功能 |
| 說明內容 | 文件描述一個找不到的畫面——比壞掉更糟,因為使用者會以為是自己找不到 |
加開關時,用全庫搜尋找出所有引用該功能的地方,逐一決定要不要 gate, 並把清單寫進開關檔的註解。
不要只藏側邊欄就宣告完成。跨功能引用是後台系統的常態—— 一個功能之所以值得做,通常正是因為別的畫面會用到它。
4. 前置條件:先證明核心數字零依賴
關掉一個功能之前,要先回答一個問題: 核心計算結果會不會因此改變?
被關掉的資料 ──?──> 核心計算
(若有依賴,關功能=改數字,那不是關功能,是改行為)
驗證方式很直接:搜尋核心計算與選取器對那些資料的引用,確認是零。 不是零的話,開關就不是「隱藏入口」而是「改變結果」—— 這種情況要嘛先把依賴切乾淨,要嘛就不要用開關解決。
這條檢查值得寫進提交說明,因為它是下一個人重新打開開關時最想知道的事。
5. 邊界要定義精確
同名的東西不見得屬於同一個功能。
舉例:關掉「任務追蹤」功能之後,主檔裡那個叫「任務類別」的自由文字欄位、 以及報表裡依它分組的功能,都應該留著—— 那是一個獨立的分類維度,只是名字剛好一樣。
在開關的註解裡寫明它不包含什麼。
不要靠關鍵字搜尋決定範圍。「凡是出現這個詞的都關掉」會連帶關掉 一個使用者天天在用、而且與該功能毫無關係的欄位。
6. 重啟=改一個布林值
整套做法的驗收標準只有一句:把 false 改成 true,功能完整回來,不需要其他改動。
做不到的話,代表某個地方的隱藏不是靠開關,而是靠刪除或註解—— 那些地方會在重啟時變成驚喜。
取捨
代價一:被關掉的程式碼仍然要維護。
它會被重構掃到、會被相依套件升級影響、會讓建置產物變大。 這是刻意付的成本——換來的是「重啟」是一個決定而不是一個專案。
代價二:開關會累積。
沒有人清理的開關,三年後會有二十個,而且沒人知道哪些還有意義。 減災方式:每個開關的註解要寫誰決定的、為什麼、什麼條件下打開。 第三項最重要——沒有打開條件的開關,實際上是一個永久刪除, 只是偽裝成暫時的。
代價三:測試矩陣變兩倍(理論上)。
實務上不必真的跑兩套:核心計算既然已證明零依賴, 測試只要涵蓋「開關關閉時導覽與路由仍然正確」即可, 其餘測試沿用原本那一套。
反例
開關的粒度=一個使用者認得的功能。
不要用開關包單一按鈕或單一欄位。那不叫功能開關, 那是把「要不要做這個設計」的決定用程式碼記下來——該決定的事就決定掉。
關閉的功能在元件層仍然可被預覽(Storybook), 設計與實作可以繼續推進。
不要因為「反正關掉了」就停止維護它的 story 與測試。 重啟那天你會發現它已經與現行的元件庫脫鉤了, 而那正是你當初選擇用開關而不是刪除的唯一理由。