<?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>Cost-Thinking on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cost-thinking/</link><description>Recent content in Cost-Thinking on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cost-thinking/index.xml" rel="self" type="application/rss+xml"/><item><title>成本思維：軟體開發的隱性代價</title><link>https://tarrragon.github.io/blog/python/00-philosophy/cost-thinking/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/00-philosophy/cost-thinking/</guid><description>&lt;h2 id="什麼是軟體開發的成本">什麼是軟體開發的成本？&lt;/h2>
&lt;p>當我們談論軟體開發的「成本」，大多數人想到的是開發時間：「這個功能需要多少工時？」&lt;/p>
&lt;p>但這只是冰山一角。&lt;/p>
&lt;h3 id="顯性成本-vs-隱性成本">顯性成本 vs 隱性成本&lt;/h3>
&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>修改 11 處重複程式碼&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復成本&lt;/td>
 &lt;td>自訂實作引入 bug 後的 hotfix&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;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;/p>
&lt;h3 id="成本思維的核心問題">成本思維的核心問題&lt;/h3>
&lt;p>每次做技術決策時，問自己：&lt;/p>
&lt;blockquote>
&lt;p>這個決策的「總成本」是多少？不只是現在的開發成本，還包括未來的維護、修復、擴展成本。&lt;/p>&lt;/blockquote>
&lt;p>這就是成本思維的本質：&lt;strong>把時間軸拉長來評估決策。&lt;/strong>&lt;/p>
&lt;h2 id="重新造輪子的真實成本">重新造輪子的真實成本&lt;/h2>
&lt;h3 id="一個看似合理的決策">一個看似合理的決策&lt;/h3>
&lt;p>假設你需要一個「延遲建立檔案」的日誌 Handler &amp;ndash; 只有在真正寫入日誌時才建立檔案，避免產生空的日誌檔。&lt;/p>
&lt;p>你可能會這樣想：「標準庫的 FileHandler 不支援延遲建立，我自己寫一個。」&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 自訂實作（看似合理，實則隱藏成本）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">LazyFileHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">FileHandler&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;延遲建立檔案的 Handler&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">filename&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encoding&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">filename&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mode&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">_file_created&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1"># 不呼叫 super().__init__() 以避免建立檔案&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Handler&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="nf">emit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">record&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">_file_created&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">makedirs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dirname&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">filename&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">exist_ok&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">_file_created&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nb">super&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">emit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">record&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="c1"># AttributeError: &amp;#39;LazyFileHandler&amp;#39; has no attribute &amp;#39;stream&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="隱藏的成本鏈">隱藏的成本鏈&lt;/h3>
&lt;p>這段程式碼引發了一連串的成本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">1. 開發成本：寫自訂類別 ~30 分鐘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. 除錯成本：追蹤 AttributeError ~1 小時
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. 修復成本：派發 hotfix 任務 ~2 小時
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. 驗證成本：確認修復後無迴歸 ~30 分鐘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">─────────────────────────────────
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> 總成本：~4 小時&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="標準庫方案">標準庫方案&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 一行解決&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">handler&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">FileHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">filename&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">delay&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># delay=True：延遲到第一次 emit 時才建立檔案&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Python 3.0 就已存在，經過 15+ 年的穩定性驗證&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開發成本：約 1 分鐘。維護成本：零。修復成本：零。&lt;/p>
&lt;h3 id="成本對比">成本對比&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>自訂 LazyFileHandler&lt;/th>
 &lt;th>標準庫 delay=True&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>開發時間&lt;/td>
 &lt;td>30 分鐘&lt;/td>
 &lt;td>1 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式碼行數&lt;/td>
 &lt;td>20+ 行&lt;/td>
 &lt;td>1 行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試需求&lt;/td>
 &lt;td>需要自行測試&lt;/td>
 &lt;td>標準庫已驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 風險&lt;/td>
 &lt;td>高（跳過 super 初始化）&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;tr>
 &lt;td>總成本&lt;/td>
 &lt;td>~4 小時&lt;/td>
 &lt;td>~1 分鐘&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>教訓：在寫任何自訂實作之前，先花 5 分鐘搜尋標準庫。這 5 分鐘的投資，可能節省數小時的維護和除錯成本。&lt;/p>
&lt;h2 id="重複程式碼的累積成本">重複程式碼的累積成本&lt;/h2>
&lt;h3 id="從-1-處到-11-處">從 1 處到 11 處&lt;/h3>
&lt;p>一個簡單的函式，從 stdin 讀取 JSON：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 這段程式碼出現在 11 個 Hook 檔案中&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">read_json_from_stdin&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdin&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當它只出現在 1 個檔案中時，問題不大。但隨著 Hook 數量增加，這段程式碼被複製到了 11 個檔案。&lt;/p>
&lt;h3 id="累積成本的計算">累積成本的計算&lt;/h3>
&lt;p>假設有一天你需要修改這個函式的行為（例如加入錯誤日誌記錄）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改後的版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">read_json_from_stdin&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kn">import&lt;/span> &lt;span class="nn">sys&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">json&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdin&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">JSONDecodeError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">warning&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stdin JSON 解析失敗: &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stdin 讀取異常: &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>1 份程式碼&lt;/th>
 &lt;th>11 份重複&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>修改次數&lt;/td>
 &lt;td>1&lt;/td>
 &lt;td>11&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試次數&lt;/td>
 &lt;td>1&lt;/td>
 &lt;td>11&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遺漏風險&lt;/td>
 &lt;td>0%&lt;/td>
 &lt;td>~20%（經驗值）&lt;/td>
 &lt;/tr>
 &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;h3 id="指數增長的維護成本">指數增長的維護成本&lt;/h3>
&lt;p>重複程式碼的成本隨著時間呈指數增長：&lt;/p></description><content:encoded><![CDATA[<h2 id="什麼是軟體開發的成本">什麼是軟體開發的成本？</h2>
<p>當我們談論軟體開發的「成本」，大多數人想到的是開發時間：「這個功能需要多少工時？」</p>
<p>但這只是冰山一角。</p>
<h3 id="顯性成本-vs-隱性成本">顯性成本 vs 隱性成本</h3>
<table>
  <thead>
      <tr>
          <th>成本類型</th>
          <th>例子</th>
          <th>容易被看見？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開發時間</td>
          <td>寫程式碼、除錯</td>
          <td>是</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>修改 11 處重複程式碼</td>
          <td>否</td>
      </tr>
      <tr>
          <td>修復成本</td>
          <td>自訂實作引入 bug 後的 hotfix</td>
          <td>否</td>
      </tr>
      <tr>
          <td>失敗成本</td>
          <td>任務失敗後的重試和浪費</td>
          <td>否</td>
      </tr>
      <tr>
          <td>基礎設施債務</td>
          <td>缺乏可觀測性導致的除錯時間</td>
          <td>否</td>
      </tr>
      <tr>
          <td>設計決策的長期代價</td>
          <td>選擇了不適當的清理頻率</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p>隱性成本的特點是：決策當下看不見，但會在未來反覆出現。</p>
<h3 id="成本思維的核心問題">成本思維的核心問題</h3>
<p>每次做技術決策時，問自己：</p>
<blockquote>
<p>這個決策的「總成本」是多少？不只是現在的開發成本，還包括未來的維護、修復、擴展成本。</p></blockquote>
<p>這就是成本思維的本質：<strong>把時間軸拉長來評估決策。</strong></p>
<h2 id="重新造輪子的真實成本">重新造輪子的真實成本</h2>
<h3 id="一個看似合理的決策">一個看似合理的決策</h3>
<p>假設你需要一個「延遲建立檔案」的日誌 Handler &ndash; 只有在真正寫入日誌時才建立檔案，避免產生空的日誌檔。</p>
<p>你可能會這樣想：「標準庫的 FileHandler 不支援延遲建立，我自己寫一個。」</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 自訂實作（看似合理，實則隱藏成本）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">LazyFileHandler</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">FileHandler</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s2">&#34;&#34;&#34;延遲建立檔案的 Handler&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s1">&#39;a&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">filename</span> <span class="o">=</span> <span class="n">filename</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">mode</span> <span class="o">=</span> <span class="n">mode</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_file_created</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="c1"># 不呼叫 super().__init__() 以避免建立檔案</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">logging</span><span class="o">.</span><span class="n">Handler</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">def</span> <span class="nf">emit</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">record</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_file_created</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="n">os</span><span class="o">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">filename</span><span class="p">),</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">_file_created</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="c1"># AttributeError: &#39;LazyFileHandler&#39; has no attribute &#39;stream&#39;</span></span></span></code></pre></div><h3 id="隱藏的成本鏈">隱藏的成本鏈</h3>
<p>這段程式碼引發了一連串的成本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 開發成本：寫自訂類別          ~30 分鐘
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 除錯成本：追蹤 AttributeError  ~1 小時
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 修復成本：派發 hotfix 任務      ~2 小時
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 驗證成本：確認修復後無迴歸      ~30 分鐘
</span></span><span class="line"><span class="ln">5</span><span class="cl">─────────────────────────────────
</span></span><span class="line"><span class="ln">6</span><span class="cl">   總成本：~4 小時</span></span></code></pre></div><h3 id="標準庫方案">標準庫方案</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 一行解決</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">handler</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">FileHandler</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">delay</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># delay=True：延遲到第一次 emit 時才建立檔案</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Python 3.0 就已存在，經過 15+ 年的穩定性驗證</span></span></span></code></pre></div><p>開發成本：約 1 分鐘。維護成本：零。修復成本：零。</p>
<h3 id="成本對比">成本對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自訂 LazyFileHandler</th>
          <th>標準庫 delay=True</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開發時間</td>
          <td>30 分鐘</td>
          <td>1 分鐘</td>
      </tr>
      <tr>
          <td>程式碼行數</td>
          <td>20+ 行</td>
          <td>1 行</td>
      </tr>
      <tr>
          <td>測試需求</td>
          <td>需要自行測試</td>
          <td>標準庫已驗證</td>
      </tr>
      <tr>
          <td>Bug 風險</td>
          <td>高（跳過 super 初始化）</td>
          <td>極低</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>需要持續維護</td>
          <td>零</td>
      </tr>
      <tr>
          <td>總成本</td>
          <td>~4 小時</td>
          <td>~1 分鐘</td>
      </tr>
  </tbody>
</table>
<p>教訓：在寫任何自訂實作之前，先花 5 分鐘搜尋標準庫。這 5 分鐘的投資，可能節省數小時的維護和除錯成本。</p>
<h2 id="重複程式碼的累積成本">重複程式碼的累積成本</h2>
<h3 id="從-1-處到-11-處">從 1 處到 11 處</h3>
<p>一個簡單的函式，從 stdin 讀取 JSON：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 這段程式碼出現在 11 個 Hook 檔案中</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">read_json_from_stdin</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">return</span> <span class="p">{}</span></span></span></code></pre></div><p>當它只出現在 1 個檔案中時，問題不大。但隨著 Hook 數量增加，這段程式碼被複製到了 11 個檔案。</p>
<h3 id="累積成本的計算">累積成本的計算</h3>
<p>假設有一天你需要修改這個函式的行為（例如加入錯誤日誌記錄）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 修改後的版本</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">read_json_from_stdin</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">json</span><span class="o">,</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;stdin JSON 解析失敗: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;stdin 讀取異常: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="p">{}</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>維度</th>
          <th>1 份程式碼</th>
          <th>11 份重複</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修改次數</td>
          <td>1</td>
          <td>11</td>
      </tr>
      <tr>
          <td>測試次數</td>
          <td>1</td>
          <td>11</td>
      </tr>
      <tr>
          <td>遺漏風險</td>
          <td>0%</td>
          <td>~20%（經驗值）</td>
      </tr>
      <tr>
          <td>行為不一致風險</td>
          <td>無</td>
          <td>有</td>
      </tr>
      <tr>
          <td>程式碼審查成本</td>
          <td>低</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<h3 id="指數增長的維護成本">指數增長的維護成本</h3>
<p>重複程式碼的成本隨著時間呈指數增長：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">第 1 次修改：11 處 x 5 分鐘 = 55 分鐘
</span></span><span class="line"><span class="ln">2</span><span class="cl">第 2 次修改：11 處 x 5 分鐘 + 排查第 1 次遺漏的 bug = 75 分鐘
</span></span><span class="line"><span class="ln">3</span><span class="cl">第 3 次修改：11 處 x 5 分鐘 + 排查前兩次的行為不一致 = 120 分鐘
</span></span><span class="line"><span class="ln">4</span><span class="cl">...</span></span></code></pre></div><p>每次遺漏一處修改，就會引入一個「行為不一致」的隱性 bug。這些 bug 不會立即爆發，而是在某個不相關的除錯過程中突然出現，讓你花數小時追蹤一個「不應該存在」的問題。</p>
<h3 id="正確做法提前提取">正確做法：提前提取</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># lib/hook_io.py（共用模組）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">read_json_from_stdin</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">    從 stdin 讀取 JSON 資料。
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">    Returns:
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">        解析後的字典，失敗時返回空字典
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">json</span><span class="o">,</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">except</span> <span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;stdin JSON 解析失敗: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;stdin 讀取異常: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">return</span> <span class="p">{}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 每個 Hook 檔案中</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.hook_io</span> <span class="kn">import</span> <span class="n">read_json_from_stdin</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">input_data</span> <span class="o">=</span> <span class="n">read_json_from_stdin</span><span class="p">()</span></span></span></code></pre></div><p>修改 1 處，所有 11 個 Hook 自動生效。</p>
<p>教訓：DRY 不只是「不要重複自己」的美學追求，而是一個成本控制策略。重複程式碼的維護成本會隨時間加速增長。</p>
<h2 id="可觀測性看不見的基礎設施">可觀測性：看不見的基礎設施</h2>
<h3 id="一個真實的場景">一個真實的場景</h3>
<p>想像一個有 20 個 Hook 的系統，某天你發現有 7 個 Hook 靜默失敗了 &ndash; 沒有錯誤訊息，沒有日誌，就是安靜地不做事。而且這個情況已經持續了至少 2 個 session（數小時）。</p>
<p>你怎麼發現的？靠偶然的手動檢查，監控系統沒有抓到。</p>
<h3 id="為什麼會靜默失敗">為什麼會靜默失敗？</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 「安全」的錯誤處理（實際上是最危險的）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">run_hook_safely</span><span class="p">(</span><span class="n">hook_func</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">hook_func</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="c1"># 只寫入檔案日誌，不通知任何人</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">log_to_file</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Hook 失敗: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div><p>這段程式碼的意圖是「不要讓 Hook 失敗影響主流程」。但它的副作用是：<strong>你完全不知道 Hook 有沒有在正常運作。</strong></p>
<h3 id="沒有可觀測性的除錯成本">沒有可觀測性的除錯成本</h3>
<p>當問題最終被發現時，除錯過程是這樣的：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 發現問題            0 分鐘（偶然發現，否則可能更久）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 確認哪些 Hook 失敗    30 分鐘（需要手動逐一檢查）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 找到失敗原因          2 小時（沒有日誌可看，只能猜測）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 修復失敗的 Hook       1 小時
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 驗證修復效果          30 分鐘
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 確認沒有其他受影響的部分  1 小時
</span></span><span class="line"><span class="ln">7</span><span class="cl">─────────────────────────
</span></span><span class="line"><span class="ln">8</span><span class="cl">   總成本：~5 小時（且可能仍有遺漏）</span></span></code></pre></div><h3 id="有可觀測性的除錯成本">有可觀測性的除錯成本</h3>
<p>如果一開始就投資可觀測性基礎設施：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">run_hook_safely</span><span class="p">(</span><span class="n">hook_func</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">hook_func</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="c1"># 寫入檔案日誌（完整追蹤）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">log_to_file</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Hook 失敗: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">traceback</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="c1"># 輸出到 stderr（確保使用者可見）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Error] </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span></span></span></code></pre></div><p>除錯過程變成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 發現問題           0 分鐘（stderr 立即可見）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 確認失敗原因        5 分鐘（日誌有完整的 traceback）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 修復失敗的 Hook     30 分鐘
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 驗證修復效果        10 分鐘
</span></span><span class="line"><span class="ln">5</span><span class="cl">─────────────────────────
</span></span><span class="line"><span class="ln">6</span><span class="cl">   總成本：~45 分鐘</span></span></code></pre></div><h3 id="投資回報分析">投資回報分析</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>無可觀測性</th>
          <th>有可觀測性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>前期投資</td>
          <td>0 小時</td>
          <td>~8 小時（建設日誌架構）</td>
      </tr>
      <tr>
          <td>每次除錯</td>
          <td>~5 小時</td>
          <td>~45 分鐘</td>
      </tr>
      <tr>
          <td>3 次事故後總成本</td>
          <td>15 小時</td>
          <td>8 + 2.25 = 10.25 小時</td>
      </tr>
      <tr>
          <td>5 次事故後總成本</td>
          <td>25 小時</td>
          <td>8 + 3.75 = 11.75 小時</td>
      </tr>
      <tr>
          <td>問題發現延遲</td>
          <td>數小時到數天</td>
          <td>即時</td>
      </tr>
  </tbody>
</table>
<p>只要遇到 3 次以上的事故，可觀測性投資就開始回本。而在任何有一定規模的系統中，問題出現 3 次幾乎是必然的。</p>
<p>教訓：可觀測性是「看不見的基礎設施」。它的缺失不會直接造成 bug，但會讓每個 bug 的修復成本倍增。</p>
<h2 id="系統設計中的頻率取捨">系統設計中的頻率取捨</h2>
<h3 id="問題背景">問題背景</h3>
<p>一個 Hook 系統每次執行都會產生日誌檔案。隨著時間累積，過期的日誌需要被清理。問題是：<strong>多久清理一次？</strong></p>
<h3 id="三種方案的成本比較">三種方案的成本比較</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 方案 A：每次都清理</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">run_hook</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">execute_hook_logic</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">cleanup_old_logs</span><span class="p">()</span>  <span class="c1"># 每次 Hook 執行後都清理</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 方案 B：每 N 次清理一次</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">run_hook</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">execute_hook_logic</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">state</span><span class="p">[</span><span class="s2">&#34;execution_count&#34;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="n">state</span><span class="p">[</span><span class="s2">&#34;execution_count&#34;</span><span class="p">]</span> <span class="o">%</span> <span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">cleanup_old_logs</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 方案 C：外部排程清理</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 由 cron job 或系統排程器負責</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># Hook 本身不做任何清理</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>維度</th>
          <th>方案 A：每次清理</th>
          <th>方案 B：每 N 次</th>
          <th>方案 C：外部排程</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>I/O 成本</td>
          <td>高（每次都掃描目錄）</td>
          <td>低（每 10 次一次）</td>
          <td>零（Hook 無關）</td>
      </tr>
      <tr>
          <td>精確度</td>
          <td>高（即時清理）</td>
          <td>中（最多延遲 10 次）</td>
          <td>高（可設定精確排程）</td>
      </tr>
      <tr>
          <td>複雜度</td>
          <td>低</td>
          <td>中（需要計數器）</td>
          <td>高（需要外部依賴）</td>
      </tr>
      <tr>
          <td>對 Hook 效能影響</td>
          <td>有（每次增加 I/O）</td>
          <td>小</td>
          <td>無</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>低</td>
          <td>低</td>
          <td>中（需維護排程設定）</td>
      </tr>
  </tbody>
</table>
<h3 id="決策依據找到平衡點">決策依據：找到平衡點</h3>
<p>方案 B 被選中，原因是：</p>
<ol>
<li><strong>I/O 成本可控</strong> &ndash; 每 10 次才觸發一次，對效能影響極小</li>
<li><strong>精確度可接受</strong> &ndash; 日誌多存留幾次不是關鍵問題</li>
<li><strong>零外部依賴</strong> &ndash; 不需要額外的 cron 配置</li>
<li><strong>實作簡單</strong> &ndash; 一個計數器加一個 if 判斷</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">maybe_cleanup_logs</span><span class="p">(</span><span class="n">execution_count</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">log_dir</span><span class="p">:</span> <span class="n">Path</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">    根據執行次數決定是否清理舊日誌。
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    每 LOG_CLEANUP_TRIGGER_FREQUENCY 次觸發一次清理，
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    在精確度和 I/O 成本之間取得平衡。
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="n">execution_count</span> <span class="o">%</span> <span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">cleanup_old_logs</span><span class="p">(</span><span class="n">log_dir</span><span class="p">)</span></span></span></code></pre></div><p>教訓：「最佳方案」不存在，只有「在當前限制條件下成本最低的方案」。頻率問題的本質是精確度和成本之間的取捨。</p>
<h2 id="失敗的成本">失敗的成本</h2>
<h3 id="預驗證-vs-失敗重試">預驗證 vs 失敗重試</h3>
<p>在派發任務之前，有一個關鍵的成本決策：<strong>是否先驗證任務的可行性？</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 方案 A：直接執行，失敗再處理</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">dispatch_task</span><span class="p">(</span><span class="n">task</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">execute</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>  <span class="c1"># 消耗資源</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">except</span> <span class="ne">PermissionError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="c1"># 失敗了，資源已經浪費</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">log</span><span class="p">(</span><span class="s2">&#34;任務失敗：權限不足&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 方案 B：預先驗證</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">dispatch_task</span><span class="p">(</span><span class="n">task</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">has_required_permissions</span><span class="p">(</span><span class="n">task</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">log</span><span class="p">(</span><span class="s2">&#34;跳過：權限不足&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">execute</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>  <span class="c1"># 確認可行才消耗資源</span></span></span></code></pre></div><h3 id="真實場景">真實場景</h3>
<p>兩個探索任務被派發去存取跨專案的資源，但都因為權限限制而失敗。每個任務各消耗了大量運算資源，但結果為零 &ndash; 完全浪費。</p>
<p>如果在派發前花 1 分鐘確認權限，就能避免這些浪費。</p>
<h3 id="預驗證的成本公式">預驗證的成本公式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">預驗證成本 = 驗證時間 x 每次派發
</span></span><span class="line"><span class="ln">2</span><span class="cl">失敗成本 = 任務執行時間 x 失敗機率
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">當 失敗成本 &gt; 預驗證成本 時，預驗證是值得的</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>場景</th>
          <th>預驗證成本</th>
          <th>失敗成本</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>快速本地操作</td>
          <td>高（相對於操作本身）</td>
          <td>低</td>
          <td>不需預驗證</td>
      </tr>
      <tr>
          <td>耗時遠端操作</td>
          <td>低（相對於操作本身）</td>
          <td>高</td>
          <td>必須預驗證</td>
      </tr>
      <tr>
          <td>高失敗率操作</td>
          <td>低</td>
          <td>高</td>
          <td>必須預驗證</td>
      </tr>
      <tr>
          <td>低失敗率操作</td>
          <td>中</td>
          <td>低</td>
          <td>視情況而定</td>
      </tr>
  </tbody>
</table>
<p>教訓：失敗不是免費的。每次失敗都消耗了資源、時間和注意力。預驗證是一種「用小成本避免大浪費」的投資。</p>
<h2 id="歸納成本思維的核心原則">歸納：成本思維的核心原則</h2>
<h3 id="原則一計算總成本不只是開發成本">原則一：計算總成本，不只是開發成本</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">總成本 = 開發成本 + 維護成本 + 修復成本 + 機會成本</span></span></code></pre></div><p>一個「快速完成」的方案，如果未來每次修改都要花 3 倍時間，那它其實是最昂貴的方案。</p>
<h3 id="原則二重複的成本會指數增長">原則二：重複的成本會指數增長</h3>
<p>每一份重複的程式碼都是一顆定時炸彈。它的爆炸威力隨著修改次數和時間而增長。</p>
<h3 id="原則三先搜尋再建造">原則三：先搜尋再建造</h3>
<p>在寫任何自訂實作之前，先花 5 分鐘搜尋：</p>
<ul>
<li>標準庫有沒有這個功能？</li>
<li>專案中有沒有類似的實作？</li>
<li>有沒有經過驗證的第三方方案？</li>
</ul>
<p>這 5 分鐘的搜尋成本，遠低於自訂實作可能帶來的維護成本。</p>
<h3 id="原則四可觀測性是必要投資">原則四：可觀測性是必要投資</h3>
<p>看不見的問題成本最高。因為：</p>
<ul>
<li>你不知道它存在（發現成本高）</li>
<li>你不知道它影響多大（評估成本高）</li>
<li>你不知道它什麼時候開始的（追溯成本高）</li>
</ul>
<h3 id="原則五找到取捨的平衡點">原則五：找到取捨的平衡點</h3>
<p>很少有決策是「A 絕對比 B 好」。更多的情況是：</p>
<blockquote>
<p>A 在維度 X 上更好，B 在維度 Y 上更好。</p></blockquote>
<p>成本思維是在限制條件下找到<strong>總成本最低的方案</strong>。</p>
<h3 id="原則六失敗有成本預防是投資">原則六：失敗有成本，預防是投資</h3>
<p>每次失敗都消耗資源。適當的預驗證和防護措施是一種投資 &ndash; 用確定的小成本，避免不確定的大損失。</p>
<h2 id="自我檢查清單">自我檢查清單</h2>
<p>做技術決策時，問自己這些問題：</p>
<ul>
<li><input disabled="" type="checkbox"> 這個方案的維護成本是多少？（不只是開發成本）</li>
<li><input disabled="" type="checkbox"> 標準庫或現有程式碼中有沒有類似的解決方案？</li>
<li><input disabled="" type="checkbox"> 這段程式碼會被複製到其他地方嗎？（DRY 風險）</li>
<li><input disabled="" type="checkbox"> 如果這裡出了問題，我能多快發現？（可觀測性）</li>
<li><input disabled="" type="checkbox"> 這個任務失敗的成本是多少？需要預驗證嗎？</li>
<li><input disabled="" type="checkbox"> 頻率設計是否在精確度和成本之間取得平衡？</li>
</ul>
<h2 id="小結">小結</h2>
<p>成本思維是把時間軸拉長來做決策。</p>
<p>很多「快速」的決策，在長期看來是最昂貴的。而很多看似「多餘」的投資（可觀測性、共用模組、預驗證），在長期看來反而是成本最低的選擇。</p>
<p>軟體開發不只是寫程式碼 &ndash; 它是在有限資源下做出無數個取捨決策。理解每個決策的隱性成本，才能做出真正「划算」的選擇。</p>
<blockquote>
<p>最便宜的 bug 是那個從未被寫出來的 bug。</p></blockquote>
<hr>
<h2 id="延伸閱讀">延伸閱讀</h2>
<ul>
<li><a href="/blog/python/00-philosophy/cognitive-load/" data-link-title="認知負擔：程式碼設計的核心目的" data-link-desc="所有設計原則的統一視角：降低閱讀者的認知負擔">認知負擔：程式碼設計的核心目的</a> - 認知負擔也是一種「隱性成本」</li>
<li><a href="/blog/python/00-philosophy/naming-art/" data-link-title="命名的藝術：讓程式碼說故事" data-link-desc="透過命名降低認知負擔，讓程式碼像故事一樣易讀">命名的藝術：讓程式碼說故事</a> - 好的命名降低閱讀成本</li>
<li><a href="/blog/python/00-philosophy/open-closed-principle/" data-link-title="開放封閉原則與認知負擔" data-link-desc="從認知負擔的視角重新理解 SOLID 原則">開放封閉原則與認知負擔</a> - OCP 降低擴展成本</li>
<li><a href="/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a> - 重複程式碼的成本控制實戰</li>
<li><a href="/blog/python/05-error-testing/observability-design/" data-link-title="5.6 Hook 系統可觀測性設計" data-link-desc="日誌架構、錯誤可見性、健康監控：讓 44 個 Hook 的運行狀態透明可追蹤">Hook 系統可觀測性設計</a> - 可觀測性投資的詳細案例</li>
</ul>
<hr>
<h2 id="參考資料">參考資料</h2>
<ul>
<li>McConnell, S. (2004). &ldquo;Code Complete: A Practical Handbook of Software Construction&rdquo;</li>
<li>Forsgren, N., Humble, J., &amp; Kim, G. (2018). &ldquo;Accelerate: The Science of Lean Software and DevOps&rdquo;</li>
</ul>
]]></content:encoded></item></channel></rss>