寫入端驗證與讀取端驗證看起來是同一件事的兩個位置,而它們的射程差別很大:寫入端只保護還沒出錯的機器,讀取端連已經出錯的機器一起保護。對已經出貨的裝置,這個差別決定了一次錯誤設定是可以自動恢復,還是需要有人到現場。

讓機器變磚的是持久化加自動套用,不是那個錯誤本身

單看一次選錯裝置,後果是「印不出來」——使用者重選一次就好。把它升級成不可恢復的,是兩個常見而各自合理的設計決定疊在一起:

設定會被持久化。 使用者設定過的印表機,下次不必再設一次。

設定在啟動時自動套用。 開機或登入後自動連線,讓使用者不必每天手動連。

兩者單獨都沒有問題,而合起來產生一條路徑:錯誤的裝置一旦被存進設定,每次啟動都會自動連上它。連上的若是觸控面板,開機即失去觸控——應用程式要與一個 USB 裝置通訊,得先取得該介面的獨佔使用權,而螢幕的觸控面板本身就是一個 USB 裝置;獨佔一旦成立,作業系統原本經由它接收觸碰的那條路徑就停止運作。而失去觸控就進不了設定頁改回來。(這個機制的其餘後果與四種「畫面凍結」的鑑別,見claim 一個介面等於接管它。)破壞的手段與修復的手段是同一個,於是機器停在一個自己出不來的狀態。

這條推導有一個可以一般化的形態:任何會持久化、且啟動時自動生效的設定,若它可能破壞使用者操作機器的手段,就必須有自我脫困路徑。

寫入端把關的射程只到「還沒寫壞的機器」

發現這條路徑之後最直覺的修法是在寫入端擋住:儲存設定之前驗一次這個裝置合不合法,不合法就拒絕儲存。

這個修法對還沒出事的機器有效,而它有一個結構性的射程限制:它只在儲存的那一刻執行。已經把錯誤設定存進去的機器,它們的儲存動作發生在修法之前,新版程式裝上去之後那個動作不會重演一次——錯誤的值原封不動留在儲存區,啟動時照樣被讀出來套用。

對開發環境來說這個限制不明顯,因為開發機可以清除應用程式資料重來。對已經在店家運轉的機器,清除資料代表所有設定重設,而且需要有人到現場操作一台已經沒有觸控的機器。

讀取端驗證讓修復隨著更新自動發生

把同一套驗證邏輯放在「讀出設定、套用之前」,射程就涵蓋了已經寫壞的機器:程式啟動、讀出設定、驗證不通過、清掉這筆設定、不執行自動連線。使用者拿到的是一台觸控正常、印表機未設定的機器,而重設印表機這件事在有觸控的前提下是使用者自己做得到的。

修復因此隨著版本更新自動發生,不需要現場操作——前提是那個更新送得到。更新若靠人在機器上點擊安裝,觸控已經失效的機器收不到它,讀取端驗證這時與寫入端一樣救不回存量機群,只是換了一個送不到的理由。所以這條修法的完整形態是讀取端驗證加上一條不依賴觸控的更新路徑。而這個位置還有一個附帶的性質:讀取端的驗證每次啟動都執行,所以它同時涵蓋了設定寫入之後才發生的變化——硬體被換掉、韌體更新後裝置識別碼改變、使用者把印表機接到另一個埠。寫入端的驗證只知道寫入當下的世界。

兩端的分工

兩端都做並不衝突,而它們回答的問題不同:

位置回答的問題涵蓋的機器
寫入端這個值現在合不合法尚未寫入錯誤值的機器
讀取端這個值現在還合不合法全部,含已經寫壞的機器

寫入端的價值在於即時回饋——使用者選錯的當下就知道,不必等到下次啟動。讀取端的價值在於射程。兩者取其一時,對已出貨裝置該保留的是讀取端——這兩個位置裡只有它能讓存量機器自己恢復。

判斷標準

判斷一個設定該不該有讀取端驗證,問三件事:它會不會被持久化、它會不會在啟動時自動套用、套用錯誤的值會不會影響使用者操作這台機器的手段。三者皆是就必須有讀取端驗證。而「驗證不通過」要先分成兩種,它們的處置相反:

  • 值本身不合法(設定指向的裝置類別根本不該是印表機、或那個識別碼的形狀不對)——清掉設定並跳過自動套用。
  • 值合法而裝置此刻不在(拔掉了、沒開電、換了埠)——保留設定、跳過這一次的自動連線,不要清除。

混用這兩者的代價是實的:把「查不到裝置」當成「值不合法」,使用者每次在印表機斷電時開機都會發現設定被清空,而那正是這篇要避免的「需要有人重做一次」的變種。這條分界與取不到判斷依據時放行是同一條——判不出來與判定為否,是兩個不同的答案。

第三問最容易漏想,因為它要求列舉「使用者操作這台機器的手段」,而那份清單平常沒有人寫下來。可執行的做法是先列出這台機器上所有的輸入與輸出通道——觸控、實體按鍵、網路、顯示、印表機輸出、外接週邊——再逐條問:有沒有哪個設定的錯誤值會斷開這一條。斷得開的那幾條就是第三問的答案。

設計層面還有一個更前面的選項:讓「自動套用」這件事可以被使用者中斷。開機時給一個短暫的視窗、或提供一組不經過觸控的復原入口(實體按鍵、adb),都能把「破壞手段與修復手段是同一個」這個結構拆開。讀取端驗證處理的是已知的不合法值,而復原入口處理的是還沒被列進驗證條件的那些。