<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Design-Principle on Tarragon</title><link>https://tarrragon.github.io/blog/tags/design-principle/</link><description>Recent content in Design-Principle on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 08 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/design-principle/index.xml" rel="self" type="application/rss+xml"/><item><title>兩種錯法的代價不對稱時，過濾策略要讓失敗倒向可補救的那一邊</title><link>https://tarrragon.github.io/blog/work-log/filter-strategy-follows-asymmetric-failure-cost/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/filter-strategy-follows-asymmetric-failure-cost/</guid><description>&lt;p>過濾一組候選裝置有兩個方向：列出確定要的（白名單），或列出確定不要的（黑名單）。兩個方向的正確率可以完全相同，而&lt;strong>它們錯的時候代價不同&lt;/strong>，這個不對稱決定該選哪一個。&lt;/p>
&lt;h2 id="兩種誤判落在不同的人身上">兩種誤判落在不同的人身上&lt;/h2>
&lt;p>以印表機候選清單為例：&lt;/p>
&lt;p>&lt;strong>白名單誤擋&lt;/strong>——某台印表機的宣告不符合條件，它從清單裡消失。使用者看到的是一份沒有自己印表機的清單，沒有任何操作可以把它找回來，店家當下不能出單。這個失敗&lt;strong>使用者無法補救&lt;/strong>，而且它會打擊到一整批同型號的機器。&lt;/p>
&lt;p>&lt;strong>黑名單誤放&lt;/strong>——某個不該出現的裝置留在清單裡。使用者要主動選中它才會出事。這個失敗&lt;strong>需要人為誤選才會實現&lt;/strong>，且範圍限於選錯的那一台機器。&lt;/p>
&lt;p>兩種誤判的代價不對稱時，讓失敗落在使用者有辦法補救的那一種誤判上。所以這裡選黑名單：列出確定不是印表機的用途（HID、集線器、儲存、音訊、視訊），其餘一律放行。&lt;/p>
&lt;p>這個選擇的附帶效果正好解掉&lt;a href="../usb-class-code-lives-in-two-layers/">類別代碼的兩層問題&lt;/a>留下的難題——宣告成 &lt;code>255&lt;/code> 供應商自訂的熱感印表機不在排除清單裡，所以它留得住；而觸控面板是 HID，被精準擋掉。白名單擋掉 &lt;code>255&lt;/code> 的同時擋掉的是使用者真正要用的機器。&lt;/p>
&lt;h2 id="判斷不出來的時候放行">判斷不出來的時候放行&lt;/h2>
&lt;p>排除清單需要一條配套規則：&lt;strong>取不到判斷依據時放行，不當成不合格。&lt;/strong>&lt;/p>
&lt;p>藍牙印表機沒有 USB 類別欄位，這個欄位對它而言不存在。把「讀不到值」與「值不符合」歸成同一類處置，會讓整個藍牙路徑的裝置消失——而它們是完全合法的目標。&lt;/p>
&lt;p>這條規則的一般形態是：&lt;strong>過濾條件的射程要寫清楚，射程外的對象不套用該條件。&lt;/strong> 這裡的射程是「USB 裝置」，所以條件寫成「USB 裝置且用途落在排除清單裡才排除」，而不是「用途不在允許清單裡就排除」。兩種寫法對 USB 裝置的行為相同，對藍牙裝置的行為相反。&lt;/p>
&lt;p>同一條規則也管裝置型號的推斷：程式從機器簽章字串判斷廠牌、遇到認不得的機型時，正確的輸出是「不確定」而不是猜一個。不確定的處置是引導使用者去設定外接裝置，而不是硬套某個廠牌的專屬流程——硬套的失敗形態是靜默的（呼叫了一個不存在的服務、或送出對方看不懂的指令），而引導的失敗形態是使用者知道自己要做什麼。&lt;/p>
&lt;h2 id="藍牙與-usb-是兩套彼此獨立的分類體系">藍牙與 USB 是兩套彼此獨立的分類體系&lt;/h2>
&lt;p>USB 類別代碼只存在於 USB 裝置。傳統藍牙有自己的一套——Bluetooth Class of Device，用主類別加服務位元描述，印表機落在 Imaging 大類底下的一個位元。&lt;/p>
&lt;p>兩套體系的數值沒有對應關係，把 USB 的類別代碼拿去比對藍牙裝置是型別上成立而語意上無意義的比對。而藍牙那一套本身的可靠度也有限：國產藍牙熱感機把 Class of Device 填得不正確的情形存在（同樣是現場回報、非本文核對過的清單），據以過濾就會誤擋。&lt;/p>
&lt;p>這裡的處置是&lt;strong>把過濾條件限定在 USB 裝置，藍牙一律放行&lt;/strong>。這個決定有一個具體的依據：機器內部的硬體（觸控面板、內部集線器上的裝置）不會出現在藍牙已配對清單裡，所以藍牙路徑上根本沒有「選到自己的螢幕」這種風險，過濾在那裡沒有要防的對象。&lt;strong>這兩個理由各自獨立、任一個成立就足以支持同一個處置&lt;/strong>：宣告不可靠說的是在藍牙上過濾會做錯，而內部硬體不走藍牙說的是在藍牙上根本不必過濾。&lt;/p>
&lt;h2 id="型別對而語意錯的比對不會報錯">型別對而語意錯的比對不會報錯&lt;/h2>
&lt;p>過濾條件寫對了，清單仍然可能是空的——&lt;strong>同一個症狀有第二個成因，而它落在跨層傳遞的數值表示法上&lt;/strong>。排查「清單為什麼空」時這兩個成因都要考慮，所以它跟前面幾節屬於同一個問題的不同分支，而不是另一個主題。&lt;/p>
&lt;p>原生端把 USB 的 vendor id 以十進位字串交給上層時，寫成 &lt;code>0xABCD&lt;/code> 的那個值到上層手上是 &lt;code>&amp;quot;43981&amp;quot;&lt;/code>。用十六進位字面值寫比對規則的程式不會編譯失敗、不會拋出例外，比對單純不成立——與過濾條件寫錯的表現完全一樣。&lt;/p>
&lt;p>這一類的鑑別方式是把值印出來看，而不是重讀比對邏輯。比對邏輯讀起來是對的（那個十六進位值確實是該裝置的 vendor id），錯的是兩端對「這個欄位長什麼樣」的假設不一致，而那個假設沒有寫在任何一邊的程式碼裡。&lt;strong>上面的數值是佔位的&lt;/strong>——每一層實際用什麼表示法由那一層自己決定，要知道自己這一套是哪一種，只有把值印出來看。跨層邊界上的每個識別碼欄位都值得問一次：它在這一端是什麼型別、什麼進位、什麼大小寫。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>選白名單還是黑名單，先問兩種誤判分別落在誰身上、以及他有沒有辦法自己補救。選的方向要讓誤判落在有辦法自己補救的人身上。&lt;/p>
&lt;p>過濾條件要寫出自己的射程，射程外的對象不套用；取不到判斷依據時放行，並把「不確定」當成一個要輸出的答案，不是一個要猜掉的空缺。&lt;/p>
&lt;p>跨層邊界上的識別碼比對失效時，先印值再讀邏輯——型別對而語意錯的比對在程式碼上看起來正確。&lt;/p></description><content:encoded><![CDATA[<p>過濾一組候選裝置有兩個方向：列出確定要的（白名單），或列出確定不要的（黑名單）。兩個方向的正確率可以完全相同，而<strong>它們錯的時候代價不同</strong>，這個不對稱決定該選哪一個。</p>
<h2 id="兩種誤判落在不同的人身上">兩種誤判落在不同的人身上</h2>
<p>以印表機候選清單為例：</p>
<p><strong>白名單誤擋</strong>——某台印表機的宣告不符合條件，它從清單裡消失。使用者看到的是一份沒有自己印表機的清單，沒有任何操作可以把它找回來，店家當下不能出單。這個失敗<strong>使用者無法補救</strong>，而且它會打擊到一整批同型號的機器。</p>
<p><strong>黑名單誤放</strong>——某個不該出現的裝置留在清單裡。使用者要主動選中它才會出事。這個失敗<strong>需要人為誤選才會實現</strong>，且範圍限於選錯的那一台機器。</p>
<p>兩種誤判的代價不對稱時，讓失敗落在使用者有辦法補救的那一種誤判上。所以這裡選黑名單：列出確定不是印表機的用途（HID、集線器、儲存、音訊、視訊），其餘一律放行。</p>
<p>這個選擇的附帶效果正好解掉<a href="../usb-class-code-lives-in-two-layers/">類別代碼的兩層問題</a>留下的難題——宣告成 <code>255</code> 供應商自訂的熱感印表機不在排除清單裡，所以它留得住；而觸控面板是 HID，被精準擋掉。白名單擋掉 <code>255</code> 的同時擋掉的是使用者真正要用的機器。</p>
<h2 id="判斷不出來的時候放行">判斷不出來的時候放行</h2>
<p>排除清單需要一條配套規則：<strong>取不到判斷依據時放行，不當成不合格。</strong></p>
<p>藍牙印表機沒有 USB 類別欄位，這個欄位對它而言不存在。把「讀不到值」與「值不符合」歸成同一類處置，會讓整個藍牙路徑的裝置消失——而它們是完全合法的目標。</p>
<p>這條規則的一般形態是：<strong>過濾條件的射程要寫清楚，射程外的對象不套用該條件。</strong> 這裡的射程是「USB 裝置」，所以條件寫成「USB 裝置且用途落在排除清單裡才排除」，而不是「用途不在允許清單裡就排除」。兩種寫法對 USB 裝置的行為相同，對藍牙裝置的行為相反。</p>
<p>同一條規則也管裝置型號的推斷：程式從機器簽章字串判斷廠牌、遇到認不得的機型時，正確的輸出是「不確定」而不是猜一個。不確定的處置是引導使用者去設定外接裝置，而不是硬套某個廠牌的專屬流程——硬套的失敗形態是靜默的（呼叫了一個不存在的服務、或送出對方看不懂的指令），而引導的失敗形態是使用者知道自己要做什麼。</p>
<h2 id="藍牙與-usb-是兩套彼此獨立的分類體系">藍牙與 USB 是兩套彼此獨立的分類體系</h2>
<p>USB 類別代碼只存在於 USB 裝置。傳統藍牙有自己的一套——Bluetooth Class of Device，用主類別加服務位元描述，印表機落在 Imaging 大類底下的一個位元。</p>
<p>兩套體系的數值沒有對應關係，把 USB 的類別代碼拿去比對藍牙裝置是型別上成立而語意上無意義的比對。而藍牙那一套本身的可靠度也有限：國產藍牙熱感機把 Class of Device 填得不正確的情形存在（同樣是現場回報、非本文核對過的清單），據以過濾就會誤擋。</p>
<p>這裡的處置是<strong>把過濾條件限定在 USB 裝置，藍牙一律放行</strong>。這個決定有一個具體的依據：機器內部的硬體（觸控面板、內部集線器上的裝置）不會出現在藍牙已配對清單裡，所以藍牙路徑上根本沒有「選到自己的螢幕」這種風險，過濾在那裡沒有要防的對象。<strong>這兩個理由各自獨立、任一個成立就足以支持同一個處置</strong>：宣告不可靠說的是在藍牙上過濾會做錯，而內部硬體不走藍牙說的是在藍牙上根本不必過濾。</p>
<h2 id="型別對而語意錯的比對不會報錯">型別對而語意錯的比對不會報錯</h2>
<p>過濾條件寫對了，清單仍然可能是空的——<strong>同一個症狀有第二個成因，而它落在跨層傳遞的數值表示法上</strong>。排查「清單為什麼空」時這兩個成因都要考慮，所以它跟前面幾節屬於同一個問題的不同分支，而不是另一個主題。</p>
<p>原生端把 USB 的 vendor id 以十進位字串交給上層時，寫成 <code>0xABCD</code> 的那個值到上層手上是 <code>&quot;43981&quot;</code>。用十六進位字面值寫比對規則的程式不會編譯失敗、不會拋出例外，比對單純不成立——與過濾條件寫錯的表現完全一樣。</p>
<p>這一類的鑑別方式是把值印出來看，而不是重讀比對邏輯。比對邏輯讀起來是對的（那個十六進位值確實是該裝置的 vendor id），錯的是兩端對「這個欄位長什麼樣」的假設不一致，而那個假設沒有寫在任何一邊的程式碼裡。<strong>上面的數值是佔位的</strong>——每一層實際用什麼表示法由那一層自己決定，要知道自己這一套是哪一種，只有把值印出來看。跨層邊界上的每個識別碼欄位都值得問一次：它在這一端是什麼型別、什麼進位、什麼大小寫。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>選白名單還是黑名單，先問兩種誤判分別落在誰身上、以及他有沒有辦法自己補救。選的方向要讓誤判落在有辦法自己補救的人身上。</p>
<p>過濾條件要寫出自己的射程，射程外的對象不套用；取不到判斷依據時放行，並把「不確定」當成一個要輸出的答案，不是一個要猜掉的空缺。</p>
<p>跨層邊界上的識別碼比對失效時，先印值再讀邏輯——型別對而語意錯的比對在程式碼上看起來正確。</p>
]]></content:encoded></item><item><title>會在開機自動生效的設定要在讀取端驗證，寫入端的把關救不了已經寫壞的機器</title><link>https://tarrragon.github.io/blog/work-log/validate-persisted-settings-on-read/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/validate-persisted-settings-on-read/</guid><description>&lt;p>寫入端驗證與讀取端驗證看起來是同一件事的兩個位置，而它們的射程差別很大：&lt;strong>寫入端只保護還沒出錯的機器，讀取端連已經出錯的機器一起保護&lt;/strong>。對已經出貨的裝置，這個差別決定了一次錯誤設定是可以自動恢復，還是需要有人到現場。&lt;/p>
&lt;h2 id="讓機器變磚的是持久化加自動套用不是那個錯誤本身">讓機器變磚的是持久化加自動套用，不是那個錯誤本身&lt;/h2>
&lt;p>單看一次選錯裝置，後果是「印不出來」——使用者重選一次就好。把它升級成不可恢復的，是兩個常見而各自合理的設計決定疊在一起：&lt;/p>
&lt;p>&lt;strong>設定會被持久化。&lt;/strong> 使用者設定過的印表機，下次不必再設一次。&lt;/p>
&lt;p>&lt;strong>設定在啟動時自動套用。&lt;/strong> 開機或登入後自動連線，讓使用者不必每天手動連。&lt;/p>
&lt;p>兩者單獨都沒有問題，而合起來產生一條路徑：錯誤的裝置一旦被存進設定，每次啟動都會自動連上它。連上的若是觸控面板，開機即失去觸控——&lt;strong>應用程式要與一個 USB 裝置通訊，得先取得該介面的獨佔使用權，而螢幕的觸控面板本身就是一個 USB 裝置；獨佔一旦成立，作業系統原本經由它接收觸碰的那條路徑就停止運作&lt;/strong>。而失去觸控就進不了設定頁改回來。（這個機制的其餘後果與四種「畫面凍結」的鑑別，見&lt;a href="../usb-claim-takes-the-hardware-away/">claim 一個介面等於接管它&lt;/a>。）&lt;strong>破壞的手段與修復的手段是同一個&lt;/strong>，於是機器停在一個自己出不來的狀態。&lt;/p>
&lt;p>這條推導有一個可以一般化的形態：&lt;strong>任何會持久化、且啟動時自動生效的設定，若它可能破壞使用者操作機器的手段，就必須有自我脫困路徑。&lt;/strong>&lt;/p>
&lt;h2 id="寫入端把關的射程只到還沒寫壞的機器">寫入端把關的射程只到「還沒寫壞的機器」&lt;/h2>
&lt;p>發現這條路徑之後最直覺的修法是在寫入端擋住：儲存設定之前驗一次這個裝置合不合法，不合法就拒絕儲存。&lt;/p>
&lt;p>這個修法對還沒出事的機器有效，而它有一個結構性的射程限制：&lt;strong>它只在儲存的那一刻執行&lt;/strong>。已經把錯誤設定存進去的機器，它們的儲存動作發生在修法之前，新版程式裝上去之後那個動作不會重演一次——錯誤的值原封不動留在儲存區，啟動時照樣被讀出來套用。&lt;/p>
&lt;p>對開發環境來說這個限制不明顯，因為開發機可以清除應用程式資料重來。對已經在店家運轉的機器，清除資料代表所有設定重設，而且需要有人到現場操作一台已經沒有觸控的機器。&lt;/p>
&lt;h2 id="讀取端驗證讓修復隨著更新自動發生">讀取端驗證讓修復隨著更新自動發生&lt;/h2>
&lt;p>把同一套驗證邏輯放在「讀出設定、套用之前」，射程就涵蓋了已經寫壞的機器：程式啟動、讀出設定、驗證不通過、清掉這筆設定、不執行自動連線。使用者拿到的是一台觸控正常、印表機未設定的機器，而重設印表機這件事在有觸控的前提下是使用者自己做得到的。&lt;/p>
&lt;p>修復因此隨著版本更新自動發生，不需要現場操作——&lt;strong>前提是那個更新送得到&lt;/strong>。更新若靠人在機器上點擊安裝，觸控已經失效的機器收不到它，讀取端驗證這時與寫入端一樣救不回存量機群，只是換了一個送不到的理由。所以這條修法的完整形態是讀取端驗證加上一條不依賴觸控的更新路徑。而這個位置還有一個附帶的性質：讀取端的驗證每次啟動都執行，所以它同時涵蓋了設定寫入之後才發生的變化——硬體被換掉、韌體更新後裝置識別碼改變、使用者把印表機接到另一個埠。寫入端的驗證只知道寫入當下的世界。&lt;/p>
&lt;h2 id="兩端的分工">兩端的分工&lt;/h2>
&lt;p>兩端都做並不衝突，而它們回答的問題不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>回答的問題&lt;/th>
 &lt;th>涵蓋的機器&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫入端&lt;/td>
 &lt;td>這個值現在合不合法&lt;/td>
 &lt;td>尚未寫入錯誤值的機器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀取端&lt;/td>
 &lt;td>這個值現在還合不合法&lt;/td>
 &lt;td>全部，含已經寫壞的機器&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>寫入端的價值在於&lt;strong>即時回饋&lt;/strong>——使用者選錯的當下就知道，不必等到下次啟動。讀取端的價值在於&lt;strong>射程&lt;/strong>。兩者取其一時，對已出貨裝置該保留的是讀取端——這兩個位置裡只有它能讓存量機器自己恢復。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>判斷一個設定該不該有讀取端驗證，問三件事：它會不會被持久化、它會不會在啟動時自動套用、套用錯誤的值會不會影響使用者操作這台機器的手段。三者皆是就必須有讀取端驗證。而「驗證不通過」要先分成兩種，它們的處置相反：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>值本身不合法&lt;/strong>（設定指向的裝置類別根本不該是印表機、或那個識別碼的形狀不對）——清掉設定並跳過自動套用。&lt;/li>
&lt;li>&lt;strong>值合法而裝置此刻不在&lt;/strong>（拔掉了、沒開電、換了埠）——保留設定、跳過這一次的自動連線，不要清除。&lt;/li>
&lt;/ul>
&lt;p>混用這兩者的代價是實的：把「查不到裝置」當成「值不合法」，使用者每次在印表機斷電時開機都會發現設定被清空，而那正是這篇要避免的「需要有人重做一次」的變種。這條分界與&lt;a href="../filter-strategy-follows-asymmetric-failure-cost/">取不到判斷依據時放行&lt;/a>是同一條——&lt;strong>判不出來與判定為否，是兩個不同的答案&lt;/strong>。&lt;/p>
&lt;p>第三問最容易漏想，因為它要求列舉「使用者操作這台機器的手段」，而那份清單平常沒有人寫下來。可執行的做法是先列出這台機器上所有的輸入與輸出通道——觸控、實體按鍵、網路、顯示、印表機輸出、外接週邊——再逐條問：&lt;strong>有沒有哪個設定的錯誤值會斷開這一條&lt;/strong>。斷得開的那幾條就是第三問的答案。&lt;/p>
&lt;p>設計層面還有一個更前面的選項：讓「自動套用」這件事可以被使用者中斷。開機時給一個短暫的視窗、或提供一組不經過觸控的復原入口（實體按鍵、adb），都能把「破壞手段與修復手段是同一個」這個結構拆開。讀取端驗證處理的是已知的不合法值，而復原入口處理的是還沒被列進驗證條件的那些。&lt;/p></description><content:encoded><![CDATA[<p>寫入端驗證與讀取端驗證看起來是同一件事的兩個位置，而它們的射程差別很大：<strong>寫入端只保護還沒出錯的機器，讀取端連已經出錯的機器一起保護</strong>。對已經出貨的裝置，這個差別決定了一次錯誤設定是可以自動恢復，還是需要有人到現場。</p>
<h2 id="讓機器變磚的是持久化加自動套用不是那個錯誤本身">讓機器變磚的是持久化加自動套用，不是那個錯誤本身</h2>
<p>單看一次選錯裝置，後果是「印不出來」——使用者重選一次就好。把它升級成不可恢復的，是兩個常見而各自合理的設計決定疊在一起：</p>
<p><strong>設定會被持久化。</strong> 使用者設定過的印表機，下次不必再設一次。</p>
<p><strong>設定在啟動時自動套用。</strong> 開機或登入後自動連線，讓使用者不必每天手動連。</p>
<p>兩者單獨都沒有問題，而合起來產生一條路徑：錯誤的裝置一旦被存進設定，每次啟動都會自動連上它。連上的若是觸控面板，開機即失去觸控——<strong>應用程式要與一個 USB 裝置通訊，得先取得該介面的獨佔使用權，而螢幕的觸控面板本身就是一個 USB 裝置；獨佔一旦成立，作業系統原本經由它接收觸碰的那條路徑就停止運作</strong>。而失去觸控就進不了設定頁改回來。（這個機制的其餘後果與四種「畫面凍結」的鑑別，見<a href="../usb-claim-takes-the-hardware-away/">claim 一個介面等於接管它</a>。）<strong>破壞的手段與修復的手段是同一個</strong>，於是機器停在一個自己出不來的狀態。</p>
<p>這條推導有一個可以一般化的形態：<strong>任何會持久化、且啟動時自動生效的設定，若它可能破壞使用者操作機器的手段，就必須有自我脫困路徑。</strong></p>
<h2 id="寫入端把關的射程只到還沒寫壞的機器">寫入端把關的射程只到「還沒寫壞的機器」</h2>
<p>發現這條路徑之後最直覺的修法是在寫入端擋住：儲存設定之前驗一次這個裝置合不合法，不合法就拒絕儲存。</p>
<p>這個修法對還沒出事的機器有效，而它有一個結構性的射程限制：<strong>它只在儲存的那一刻執行</strong>。已經把錯誤設定存進去的機器，它們的儲存動作發生在修法之前，新版程式裝上去之後那個動作不會重演一次——錯誤的值原封不動留在儲存區，啟動時照樣被讀出來套用。</p>
<p>對開發環境來說這個限制不明顯，因為開發機可以清除應用程式資料重來。對已經在店家運轉的機器，清除資料代表所有設定重設，而且需要有人到現場操作一台已經沒有觸控的機器。</p>
<h2 id="讀取端驗證讓修復隨著更新自動發生">讀取端驗證讓修復隨著更新自動發生</h2>
<p>把同一套驗證邏輯放在「讀出設定、套用之前」，射程就涵蓋了已經寫壞的機器：程式啟動、讀出設定、驗證不通過、清掉這筆設定、不執行自動連線。使用者拿到的是一台觸控正常、印表機未設定的機器，而重設印表機這件事在有觸控的前提下是使用者自己做得到的。</p>
<p>修復因此隨著版本更新自動發生，不需要現場操作——<strong>前提是那個更新送得到</strong>。更新若靠人在機器上點擊安裝，觸控已經失效的機器收不到它，讀取端驗證這時與寫入端一樣救不回存量機群，只是換了一個送不到的理由。所以這條修法的完整形態是讀取端驗證加上一條不依賴觸控的更新路徑。而這個位置還有一個附帶的性質：讀取端的驗證每次啟動都執行，所以它同時涵蓋了設定寫入之後才發生的變化——硬體被換掉、韌體更新後裝置識別碼改變、使用者把印表機接到另一個埠。寫入端的驗證只知道寫入當下的世界。</p>
<h2 id="兩端的分工">兩端的分工</h2>
<p>兩端都做並不衝突，而它們回答的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>回答的問題</th>
          <th>涵蓋的機器</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入端</td>
          <td>這個值現在合不合法</td>
          <td>尚未寫入錯誤值的機器</td>
      </tr>
      <tr>
          <td>讀取端</td>
          <td>這個值現在還合不合法</td>
          <td>全部，含已經寫壞的機器</td>
      </tr>
  </tbody>
</table>
<p>寫入端的價值在於<strong>即時回饋</strong>——使用者選錯的當下就知道，不必等到下次啟動。讀取端的價值在於<strong>射程</strong>。兩者取其一時，對已出貨裝置該保留的是讀取端——這兩個位置裡只有它能讓存量機器自己恢復。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>判斷一個設定該不該有讀取端驗證，問三件事：它會不會被持久化、它會不會在啟動時自動套用、套用錯誤的值會不會影響使用者操作這台機器的手段。三者皆是就必須有讀取端驗證。而「驗證不通過」要先分成兩種，它們的處置相反：</p>
<ul>
<li><strong>值本身不合法</strong>（設定指向的裝置類別根本不該是印表機、或那個識別碼的形狀不對）——清掉設定並跳過自動套用。</li>
<li><strong>值合法而裝置此刻不在</strong>（拔掉了、沒開電、換了埠）——保留設定、跳過這一次的自動連線，不要清除。</li>
</ul>
<p>混用這兩者的代價是實的：把「查不到裝置」當成「值不合法」，使用者每次在印表機斷電時開機都會發現設定被清空，而那正是這篇要避免的「需要有人重做一次」的變種。這條分界與<a href="../filter-strategy-follows-asymmetric-failure-cost/">取不到判斷依據時放行</a>是同一條——<strong>判不出來與判定為否，是兩個不同的答案</strong>。</p>
<p>第三問最容易漏想，因為它要求列舉「使用者操作這台機器的手段」，而那份清單平常沒有人寫下來。可執行的做法是先列出這台機器上所有的輸入與輸出通道——觸控、實體按鍵、網路、顯示、印表機輸出、外接週邊——再逐條問：<strong>有沒有哪個設定的錯誤值會斷開這一條</strong>。斷得開的那幾條就是第三問的答案。</p>
<p>設計層面還有一個更前面的選項：讓「自動套用」這件事可以被使用者中斷。開機時給一個短暫的視窗、或提供一組不經過觸控的復原入口（實體按鍵、adb），都能把「破壞手段與修復手段是同一個」這個結構拆開。讀取端驗證處理的是已知的不合法值，而復原入口處理的是還沒被列進驗證條件的那些。</p>
]]></content:encoded></item></channel></rss>