<?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>Back-End on 愷的大冒險 Kai's Adventure</title><link>https://kaiadv.com/categories/back-end/</link><description>Recent content in Back-End on 愷的大冒險 Kai's Adventure</description><generator>Hugo</generator><language>zh-tw</language><lastBuildDate>Sun, 14 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://kaiadv.com/categories/back-end/index.xml" rel="self" type="application/rss+xml"/><item><title>當 Shopify Webhook 漏接時：分散式系統的資料一致性問題</title><link>https://kaiadv.com/posts/202606014/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/202606014/</guid><description>&lt;p&gt;本文延續上一篇「防止 Shopify 線上商店與實體門市超賣」的討論，探討反向情境：當 Shopify 的 Webhook Event 漏接，導致線上訂單無法即時同步進內部資料庫時，應該如何設計系統來保障資料一致性，以及避免先下單的顧客受到損失。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="問題情境"&gt;問題情境&lt;/h1&gt;
&lt;p&gt;承接上篇的架構背景：線上訂單透過 Shopify Webhook 即時回傳至內部 DB，並有一支 Timer Program 每小時定期補漏。&lt;/p&gt;
&lt;p&gt;當 Webhook 漏接時，空窗期內可能發生以下情境：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;線上顧客成功下單（Shopify 已收款）
→ Webhook 發送失敗或漏接
→ 內部 DB 不知道這筆訂單的存在
→ 內部 DB 庫存仍顯示舊的（較高）數字
同一時間，實體門市售出同一商品
→ 扣減內部 DB 庫存
Timer 補跑，終於處理到那筆線上訂單
→ 此時庫存已不足
→ 先下單的線上顧客反而無法出貨 ❌
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個問題的核心是：Shopify 與內部 DB 是兩個獨立的系統，Webhook 是主要的同步橋樑，一旦橋樑出現延遲，兩端就會產生資料落差。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="解法方向"&gt;解法方向&lt;/h1&gt;
&lt;h2 id="方案一縮短-timer-間隔"&gt;方案一：縮短 Timer 間隔&lt;/h2&gt;
&lt;p&gt;最直覺的做法是將 Timer 的同步週期從一小時縮短，例如改為每五分鐘一次，讓系統更快發現未處理的線上訂單並補上；這個方案不需要改動架構，成本最低且能夠立即縮短風險窗口。&lt;/p&gt;
&lt;p&gt;然而它存在一個根本性的隱憂：當系統因負荷過高而開始變慢，頻繁的 Timer 仍會不斷觸發新的同步任務，不斷消耗系統資源，反而讓系統雪上加霜，最終可能完全卡死；這在系統設計上是一種 Thundering Herd（驚群效應）的變體，意指用更頻繁的 Polling 來彌補事件驅動的不足，但 Polling 本身在系統壓力大時會成為壓垮駱駝的稻草。&lt;/p&gt;
&lt;p&gt;縮短間隔治標不治本，空窗期的長短仍然取決於 Timer 的頻率，只是縮小了問題的規模。&lt;/p&gt;
&lt;h2 id="方案二timer-補跑時訂單同步優先於庫存校正"&gt;方案二：Timer 補跑時，訂單同步優先於庫存校正&lt;/h2&gt;
&lt;p&gt;這是一個執行順序的調整；Timer 補跑時應先將所有未同步的線上訂單補進內部 DB，再進行庫存比對與扣減，避免實體銷售的庫存扣減蓋過尚未被認知的線上訂單。&lt;/p&gt;</description></item><item><title>如何防止 Shopify 線上商店與實體門市的超賣問題</title><link>https://kaiadv.com/posts/202606013/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/202606013/</guid><description>&lt;p&gt;本文源自一次技術面試的討論題目，紀錄了問題的成因分析與逐步推導出的解決方案，可以作為電商系統設計的學習參考：假設公司同時擁有 Shopify 線上商店與實體銷售門市，兩個渠道共用同一套內部資料庫（庫存 DB 與訂單 DB）。&lt;/p&gt;
&lt;p&gt;現有架構如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;線上訂單&lt;/strong&gt;：Shopify 透過 Webhook Event 即時回傳至訂單資料庫。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;補漏機制&lt;/strong&gt;：由於擔心 Webhook 漏接，系統另有一支 Timer Program，定期（約每小時一次）與 Shopify 比對並同步訂單與庫存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;實體訂單&lt;/strong&gt;：門市銷售的訂單會寫入內部訂單 DB，但不會同步至 Shopify。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;資料庫的語意定義如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;內部 DB 庫存 = 全渠道實際庫存總量
內部 DB 訂單 = 線上訂單 ＋ 實體訂單（所有訂單）
Shopify 庫存 = 僅反映線上銷售，對實體銷售一無所知
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h1 id="問題根源"&gt;問題根源&lt;/h1&gt;
&lt;p&gt;由於 Timer Program 每小時才同步一次，在同步之前存在一個空窗期：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;實體門市售出商品
→ 扣減內部 DB 庫存 正確
→ Shopify 庫存尚未更新 數字虛高
線上顧客在空窗期內下單
→ Shopify 顯示庫存充足 錯誤資訊
→ 顧客成功下單 實際已超賣
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;矛盾在於：內部 DB 才是庫存的 Single Source of Truth，但 Shopify 並不知道實體門市的銷售情況，導致其顯示的庫存數字在空窗期內是虛高的。&lt;/p&gt;</description></item><item><title>用 ASP.NET + Redis + PostgreSQL 實作購票系統的併發控制</title><link>https://kaiadv.com/posts/20260607/</link><pubDate>Sun, 07 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260607/</guid><description>&lt;p&gt;前兩篇分別談了樂觀鎖與悲觀鎖的理論，以及購票系統各環節的設計決策，這篇將直接進入程式碼實作，把設計思路轉化成可以實際運行的實作。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="資料模型"&gt;資料模型&lt;/h1&gt;
&lt;p&gt;先定義三個核心 Entity，各自對應不同的鎖策略。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Ticket&lt;/code&gt; 代表票種與庫存，Entity 上掛的 &lt;code&gt;RowVersion&lt;/code&gt;（對應 PostgreSQL 的 &lt;code&gt;xmin&lt;/code&gt; 系統欄位）是&lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#optimistic-concurrency"&gt;樂觀鎖機制&lt;/a&gt;，用於後台修改票種名稱、調整票價等一般更新場景。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Order&lt;/code&gt; 代表訂單，其中狀態流轉使用樂觀鎖（Optimistic Lock），手動維護 &lt;code&gt;Version&lt;/code&gt; 整數欄位，方便在 SQL 層直接用條件更新防止重複付款。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Seat&lt;/code&gt; 代表對號座的每一個座位，記錄目前是否被鎖定、被誰鎖定、以及鎖定到什麼時間；它的鎖定狀態由 Redis 的分散式鎖（Distributed Lock）主導，&lt;code&gt;LockedByUserId&lt;/code&gt; 和 &lt;code&gt;LockedUntil&lt;/code&gt; 則作為資料庫層的備份紀錄，用於查詢座位狀態或在 Redis 異常時做補救。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Ticket.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Ticket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Id { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; EventId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; TicketType { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; AvailableQty { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// EF Core 樂觀鎖：使用 PostgreSQL xmin 系統欄位，免額外欄位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// xmin 是每次 row 被更新時自動遞增的系統欄位，完全不需要手動維護&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; [Timestamp]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;byte&lt;/span&gt;[] RowVersion { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Order.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Id { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; UserId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; OrderStatus Status { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 手動版本號，用於訂單狀態流轉的樂觀鎖&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Version { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;25&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;26&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Seat.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;27&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Seat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;28&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;29&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; SeatId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!; &lt;span style="color:#8b949e;font-style:italic"&gt;// e.g. &amp;#34;A-12&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;30&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; EventId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;31&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; SeatStatus Status { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;32&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string?&lt;/span&gt; LockedByUserId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;33&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DateTime? LockedUntil { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;34&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;35&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;36&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;enum&lt;/span&gt; OrderStatus { Pending, Paid, Cancelled, Expired }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;37&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;enum&lt;/span&gt; SeatStatus { Available, Locked, Sold }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Data/AppDbContext.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;AppDbContext&lt;/span&gt; : DbContext
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Ticket&amp;gt; Tickets =&amp;gt; Set&amp;lt;Ticket&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Order&amp;gt; Orders =&amp;gt; Set&amp;lt;Order&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Seat&amp;gt; Seats =&amp;gt; Set&amp;lt;Seat&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;protected&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;override&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 將 RowVersion 對應到 PostgreSQL 的 xmin 欄位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; builder.Entity&amp;lt;Ticket&amp;gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; .Property(t =&amp;gt; t.RowVersion)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt; .IsRowVersion()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt; .HasColumnName(&lt;span style="color:#a5d6ff"&gt;&amp;#34;xmin&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt; .HasColumnType(&lt;span style="color:#a5d6ff"&gt;&amp;#34;xid&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="redis-層庫存預扣"&gt;Redis 層：庫存預扣&lt;/h1&gt;
&lt;p&gt;單純使用 &lt;code&gt;DECR&lt;/code&gt; 可能會有 Race Condition 的風險，在「讀取 → 判斷 → 扣減」三步驟之間可能插入其他操作；使用 &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/"&gt;Lua Script&lt;/a&gt; 則能夠確保 Redis 的原子執行，使得整個 check-and-decrement 變得不可分割；更多詳情可以參考 &lt;a href="https://stackexchange.github.io/StackExchange.Redis/"&gt;StackExchange.Redis&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>為什麼購票系統需要兩種不同的鎖</title><link>https://kaiadv.com/posts/20260606/</link><pubDate>Sat, 06 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260606/</guid><description>&lt;p&gt;上一篇談了悲觀鎖（Pessimistic Lock）和樂觀鎖（Optimistic Lock）的基本概念，而這篇要把焦點放在一個具體的場景：演唱會購票系統。&lt;/p&gt;
&lt;p&gt;為什麼會選這個場景呢？因為它的併發特性非常極端：開賣瞬間可能湧入數十萬個請求，庫存有限、不能超賣、用戶又非常敏感，幾乎把所有併發控制（Concurrency Control）的挑戰都壓縮在一起了，適合用來思考鎖策略的設計。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="系統的核心矛盾"&gt;系統的核心矛盾&lt;/h1&gt;
&lt;p&gt;購票系統面臨一個根本的矛盾：速度和正確性永遠在對抗。&lt;/p&gt;
&lt;p&gt;為了正確性，你想讓每個請求排隊、一個一個處理，但這樣系統會慢到讓人放棄；為了速度，你想讓所有請求並行處理，但這樣很可能超賣或出現資料不一致；而解決這個矛盾的方式，不是選擇其中一邊，而是在不同的環節用不同的策略。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="把系統拆成幾個關鍵環節"&gt;把系統拆成幾個關鍵環節&lt;/h1&gt;
&lt;h2 id="庫存扣減這是整個系統最不能出錯的地方"&gt;庫存扣減：這是整個系統最不能出錯的地方&lt;/h2&gt;
&lt;p&gt;票券超賣是商業損失，無法事後補救；這裡需要的是一致性而不是高效能，寧願讓請求排隊等待，也必須保證每次扣減前看到的都是真實的剩餘數量。&lt;/p&gt;
&lt;p&gt;這是悲觀鎖（Pessimistic Lock）典型的使用場景；在 PostgreSQL 裡，用 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; 鎖住那行資料，讓後來的交易（Transaction）必須等待；這個鎖從查詢的當下持有，直到 Transaction 提交（Commit）或回滾（Rollback）才釋放。&lt;/p&gt;
&lt;p&gt;代價是明確的：高併發下這裡會成為瓶頸，請求會大量排隊，但在「超賣後果不可逆」的前提下，這個代價值得付。&lt;/p&gt;
&lt;h2 id="座位鎖定互斥資源必須強佔"&gt;座位鎖定：互斥資源，必須強佔&lt;/h2&gt;
&lt;p&gt;對號座的選位，是另一個互斥資源（Exclusive Resource）的場景，用戶 A 選了 A-12，在她付款完成之前，A-12 不能被用戶 B 選走。&lt;/p&gt;
&lt;p&gt;這裡的問題不只是資料庫併發，更是跨請求（Request）的狀態持有；用戶進入付款頁面後，這把鎖需要持續存在 10 分鐘，若超時便自動釋放；資料庫鎖的生命週期只存在於一個 Transaction 裡，無法做到跨 Request 持有。&lt;/p&gt;
&lt;p&gt;這時可以改用 &lt;a href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/"&gt;Redis 的分散式鎖（Distributed Lock）&lt;/a&gt;：&lt;code&gt;SET NX EX&lt;/code&gt;（不存在才設置，並附上過期時間），第一個選到這個座位的請求成功鎖定，後來的請求看到 key 已存在，直接被拒絕；過了 10 分鐘之後，Redis 的 TTL 自動刪除這個 key，座位自動釋放回可選狀態。&lt;/p&gt;
&lt;h2 id="庫存預扣在資料庫前擋掉大多數請求"&gt;庫存預扣：在資料庫前擋掉大多數請求&lt;/h2&gt;
&lt;p&gt;純粹依賴 PostgreSQL 的 &lt;code&gt;FOR UPDATE&lt;/code&gt;，在演唱會開賣瞬間會承受巨大的資料庫壓力。大量請求湧進來，每個都要等待前一個 Transaction 釋放鎖，資料庫連線很快就會耗盡。&lt;/p&gt;
&lt;p&gt;這時可以在資料庫前面加一層 Redis 的原子操作（Atomic Operation）：先在 Redis 裡做庫存預扣，用 &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/"&gt;Lua Script&lt;/a&gt; 確保「檢查庫存是否足夠」和「扣減庫存」這兩步是符合原子操作且不可分割；因此大多數「票已售罄」的請求，在這一層就被擋掉了，不需要進入資料庫，只有成功預扣的請求才會繼續往下，讓 PostgreSQL 做最終確認。&lt;/p&gt;</description></item><item><title>樂觀鎖與悲觀鎖：併發的控制</title><link>https://kaiadv.com/posts/20260605/</link><pubDate>Fri, 05 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260605/</guid><description>&lt;p&gt;併發問題的根源，大多來自「多個操作同時想動同一份資料」；當系統只有一個使用者時，這不是問題，然而流量一旦上來，很多我們以為不可能同時發生的事，就會同時發生；面對這個問題，思路分成了兩個截然不同的方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="悲觀鎖先鎖再說"&gt;悲觀鎖：先鎖再說&lt;/h1&gt;
&lt;p&gt;悲觀鎖（Pessimistic Lock）的世界觀很直接：衝突一定會發生，所以在操作資料之前就先把它鎖住，其他人等我用完再說；它像一個很謹慎的人，進房間之前先把門鎖上，做完事情才開鎖讓別人進來；在這段期間，任何人想進來都必須等待。&lt;/p&gt;
&lt;p&gt;資料庫層面最常見的實作是 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;，在查詢的當下就對那幾行資料加上排他鎖，其他 transaction 如果也想修改同一行，就必須等待前一個 transaction 結束：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;BEGIN&lt;/span&gt;;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tickets&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;event_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;AND&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ticket_type&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#39;VIP&amp;#39;&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;UPDATE&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 從這一刻起，這行資料被鎖住
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 確認有票後才扣減
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;UPDATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tickets&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;SET&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;event_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;AND&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ticket_type&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#39;VIP&amp;#39;&lt;/span&gt;;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;COMMIT&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 鎖在這裡釋放
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個做法的代價很明顯：併發效能較低，因為後來的請求都必須排隊等待；而且如果兩個 transaction 互相等待對方釋放鎖，就會發生死鎖（Deadlock）；不過在某些場景下，這是正確的選擇，因為有些錯誤是無法事後補救的。&lt;/p&gt;</description></item><item><title>依賴反轉原則（Dependency Inversion Principle, DIP）</title><link>https://kaiadv.com/posts/20260603/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260603/</guid><description>&lt;p&gt;Robert C. Martin 為這個原則提出兩條互相呼應的規則：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High-level modules should not import anything from low-level modules. Both should depend on abstractions（高階模組不應依賴低階模組，兩者都應依賴抽象）。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abstractions should not depend on details. Details should depend on abstractions（抽象不應依賴細節，細節應依賴抽象）。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這兩條規則合在一起，描述的是系統中依賴關係的方向性應該如何安排，要理解它們需要先理解「高階模組」和「低階模組」的差別。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="高階模組與低階模組"&gt;高階模組與低階模組&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高階模組（High-level modules）&lt;/strong&gt;：包含應用程式核心業務邏輯的模組，它們描述的是「這個系統要做什麼」：訂單需要被驗證、付款需要被處理、報表需要被產生；這些邏輯代表了系統存在的根本原因，是具有業務價值的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低階模組（Low-level modules）&lt;/strong&gt;：提供具體技術實作的模組，它們描述的是「這件事具體怎麼做」：資料存到 SQL Server、Email 用 SMTP 發送、圖片存到 S3；這些模組是高階模組的工具，本身通常不含業務價值，可以被替換。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在傳統的依賴方向中，高階模組直接使用（依賴）低階模組：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;OrderService（高階）→ SqlServerRepository（低階）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這意味著業務邏輯層與具體的資料庫技術「綁死」在一起；若今天想把 SQL Server 換成 PostgreSQL，就必須修改 OrderService 這個本來應該只關心業務規則的類別，DIP 要「反轉」的正是這個依賴方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係多型polymorphism與抽象abstraction"&gt;與 OOP 的關係：多型（Polymorphism）與抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;DIP 的實現依賴抽象與多型，具體的做法是在高階模組與低階模組之間插入一層抽象（介面），讓兩者都依賴這個介面，而非彼此直接依賴。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;傳統方向（高耦合）：
OrderService ─────────────────→ SqlServerRepository
（高階） （低階，具體實作）
DIP 方向（低耦合）：
OrderService ──→ IOrderRepository ←── SqlServerRepository
（高階） （抽象介面） （低階，具體實作）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 DIP 的架構下，依賴箭頭的方向發生了「反轉」：&lt;/p&gt;</description></item><item><title>介面隔離原則（Interface Segregation Principle, ISP）</title><link>https://kaiadv.com/posts/20260602/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260602/</guid><description>&lt;p&gt;&lt;strong&gt;Robert C. Martin 對這個原則的定義是：Clients should not be forced to depend upon interface methods that they do not use.（不應強迫客戶端依賴它不使用的方法）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;這裡的「客戶端」不是指使用者介面的終端用戶，而是指在程式碼中使用某個介面的類別或模組；當一個介面提供了十個方法，但某個實作類別只需要其中三個，ISP 說這樣的設計是有問題的。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係抽象abstraction"&gt;與 OOP 的關係：抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;介面（Interface）是 OOP 抽象機制的體現，抽象的目的是「只暴露必要的能力，隱藏不必要的細節」；一個設計良好的介面應該像是一份精確的「能力聲明」：「持有這個介面的物件，保證能做 X、Y、Z。」&lt;/p&gt;
&lt;p&gt;然而，若一個介面同時聲明了十幾個能力，而這些能力並非總是同時需要，這個介面就失去了精準性；實作者不得不提供所有能力的實作，即使其中有些對它毫無意義；呼叫端也無法從介面名稱判斷「這個物件究竟擅長做什麼」，因為它什麼都做。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ISP 的本質是對抽象品質的要求：介面應精簡、專一，只描述一個「角色（role）」。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="肥大介面的危害"&gt;「肥大介面」的危害&lt;/h1&gt;
&lt;p&gt;假設系統有一個 &lt;code&gt;IWorker&lt;/code&gt; 介面：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;interface&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;IWorker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Work();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Eat();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; TakeBreak();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; ReceiveSalary();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;7&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;對於人類員工來說，這四個方法都有意義；但如果在系統中引入了「工業機器人」（自動化設備），它同樣需要實作 &lt;code&gt;IWorker&lt;/code&gt;（因為它也需要執行工作），但機器人不吃飯、不休息、也不領薪水，這時機器人類別就被迫實作它根本不支援的方法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;IndustrialRobot&lt;/span&gt; : IWorker
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Work() { Console.WriteLine(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人執行工序中...&amp;#34;&lt;/span&gt;); }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 機器人不需要這些，但被強迫實作&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Eat() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不需要進食&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;7&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; TakeBreak() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不需要休息&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;8&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; ReceiveSalary() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不領薪水&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;9&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個設計同時違反了 ISP 和 LSP：&lt;code&gt;IndustrialRobot&lt;/code&gt; 宣稱自己是一個 &lt;code&gt;IWorker&lt;/code&gt;，但呼叫 &lt;code&gt;Eat()&lt;/code&gt; 會在執行期間報錯，呼叫端如果不特別防範就會有潛在風險。&lt;/p&gt;</description></item><item><title>里氏替換原則（Liskov Substitution Principle, LSP）</title><link>https://kaiadv.com/posts/20260601/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260601/</guid><description>&lt;p&gt;&lt;strong&gt;Barbara Liskov 提出的定義是： If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program（若 S 是 T 的子型別，則程式中使用 T 物件的地方可以使用 S 物件來替換，而不改變程式的任何預期屬性）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Barbara Liskov 是麻省理工學院（MIT）的電腦科學教授，這個原則由他在 1987 年的一篇論文中首次提出，後來被 Robert C. Martin 納入 SOLID 體系。&lt;/p&gt;
&lt;p&gt;用更白話的方式說：凡是可以使用父類別物件的地方，換成任何子類別的物件，程式應該要能正確執行，結果也應該要符合預期；如果換了子類別之後行為變了，那就是違反了 LSP。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係繼承inheritance"&gt;與 OOP 的關係：繼承（Inheritance）&lt;/h1&gt;
&lt;p&gt;繼承是 OOP 中強大但也容易被濫用的機制，把繼承當作「程式碼複用」的工具：「子類別可以繼承父類別的方法，這樣就不用重寫了。」這個想法本身沒有錯，但它遺漏了繼承更深層的語義。&lt;/p&gt;
&lt;p&gt;在物件導向系統中，繼承不只是程式碼的共享機制，它同時建立了一種型別關係（is-a relationship）；當寫了 &lt;code&gt;class Dog : Animal&lt;/code&gt; 時，不只是讓 &lt;code&gt;Dog&lt;/code&gt; 複用了 &lt;code&gt;Animal&lt;/code&gt; 的程式碼，同時宣告了「&lt;code&gt;Dog&lt;/code&gt; 是一種 &lt;code&gt;Animal&lt;/code&gt;」；這個宣告有一個重要的隱含意義：在任何需要 &lt;code&gt;Animal&lt;/code&gt; 的場合，都可以放入一隻 &lt;code&gt;Dog&lt;/code&gt;，而程式的行為不應改變。&lt;/p&gt;</description></item><item><title>開放封閉原則（Open–Closed Principle, OCP）</title><link>https://kaiadv.com/posts/20260531/</link><pubDate>Sun, 31 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260531/</guid><description>&lt;p&gt;&lt;strong&gt;Bertrand Meyer 在 1988 年提出、後經 Robert C. Martin 推廣的定義是：Software entities should be open for extension, but closed for modification（軟體實體應該對擴展開放，對修改封閉）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;「對擴展開放」意味著當需求出現新的變化時，我們可以為系統加入新的行為；「對修改封閉」意味著加入新行為時，我們不需要改動已經存在且正常運作的程式碼；這兩個要求乍聽矛盾，既然不能改動既有程式碼，又怎麼增加新行為呢？OCP 的概念是：透過多型與抽象，讓新行為以「新增類別」的方式注入系統，而非以「修改現有邏輯」的方式侵入系統。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係多型polymorphism與抽象abstraction"&gt;與 OOP 的關係：多型（Polymorphism）與抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;OCP 的實現幾乎完全仰賴 OOP 的多型機制，想理解這個關係要先從多型的本質說起；多型讓我們可以寫出這樣的程式碼：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// 呼叫端只知道 shape 是某種 Shape，不知道它具體是哪一種&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; DrawShape(IShape shape)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt; shape.Draw(); &lt;span style="color:#8b949e;font-style:italic"&gt;// 執行時才決定呼叫哪個 Draw()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;DrawShape&lt;/code&gt; 這個方法依賴的是抽象（&lt;code&gt;IShape&lt;/code&gt; 介面），而不是任何具體的形狀類別；未來若要新增「六邊形」，只需要新增一個實作 &lt;code&gt;IShape&lt;/code&gt; 的 &lt;code&gt;Hexagon&lt;/code&gt; 類別，不需要改動 &lt;code&gt;DrawShape&lt;/code&gt;；這就是 OCP 的運作原理：抽象是穩定的，具體實作是可以擴展的。&lt;/p&gt;
&lt;p&gt;換言之，OCP 是多型概念重要的應用場景：用「新增類別」取代「修改現有邏輯」，讓已通過測試的程式碼保持穩定。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="修改的代價"&gt;「修改」的代價&lt;/h1&gt;
&lt;p&gt;為什麼要這麼費心避免修改既有程式碼？每次修改都有潛在的風險：&lt;/p&gt;</description></item><item><title>單一職責原則（Single-Responsibility Principle, SRP）</title><link>https://kaiadv.com/posts/20260530/</link><pubDate>Sat, 30 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260530/</guid><description>&lt;p&gt;&lt;strong&gt;Robert C. Martin 對這個原則的定義是：A class should have only one reason to change（一個類別應該只有一個改變的理由）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;乍看之下這句話的意思相當直觀：一個類別只做一件事，但「改變的理由」這個措辭或許比表面上更深刻，也是 SRP 常被誤解的地方。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係封裝encapsulation"&gt;與 OOP 的關係：封裝（Encapsulation）&lt;/h1&gt;
&lt;p&gt;封裝是 OOP 的第一個基石，它的目的是將資料與操作資料的行為包裝在一起，並對外隱藏實作細節；這讓類別可以像一個「黑盒子」，使用者只需要知道「它能做什麼」，不需要知道「它是怎麼做的」。&lt;/p&gt;
&lt;p&gt;然而，封裝只解決了「如何包裝」的問題，沒有回答「應該把哪些東西包在一起」；一個把所有功能都塞進同一個類別的設計，在語法上是合法的封裝，但在設計上卻是災難性的。&lt;/p&gt;
&lt;p&gt;SRP 為封裝提供了邊界判斷的標準：同一個職責（responsibility）的程式碼才應該被封裝在同一個類別裡；換句話說，類別的邊界不應由技術上的便利性決定（這幾個 method 放在一起比較方便），而應由職責的邊界來決定（這幾個 method 服務於同一個目的、同一個改變來源）。&lt;/p&gt;
&lt;p&gt;也就是說，SRP 是封裝的昇華：不只把資料藏起來，更要讓「職責」成為劃分類別邊界的判斷依據。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="改變的理由的含義"&gt;「改變的理由」的含義&lt;/h1&gt;
&lt;p&gt;Uncle Bob 在後來的詮釋中進一步說明：一個模組應該對一個且只對一個 actor（行為者） 負責。&lt;/p&gt;
&lt;p&gt;這裡的 actor 不是指使用者介面上的角色，而是指會要求這個模組發生改變的利害關係人（stakeholder），可能是一個部門、一個業務單位、一個外部系統，或任何一個有能力提出需求變更的人或團隊。&lt;/p&gt;
&lt;p&gt;用一個具體例子來說明：假設有一個 &lt;code&gt;Employee&lt;/code&gt; 類別，它提供三個方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CalculatePay()&lt;/code&gt;：計算薪資，財務部門關心這個邏輯。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReportHours()&lt;/code&gt;：回報工時，HR 部門關心這個邏輯。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Save()&lt;/code&gt;：儲存員工資料，DBA 或資料庫團隊關心這個邏輯。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三個方法，三個不同的 actor；財務部門可能要求調整薪資計算的演算法，HR 部門可能要求修改工時的統計方式，資料庫團隊可能要求更換 ORM 框架或調整儲存格式；這三類需求彼此獨立，但因為它們全都落在同一個 &lt;code&gt;Employee&lt;/code&gt; 類別裡，任何一方的修改都有可能意外影響其他兩方，即使改動本身只涉及其中一個方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;這就是「有多個改變的理由」的意涵：一個類別因為服務多個 actor，所以有多個獨立的、互不相關的原因可能促使它發生改變。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="程式碼範例"&gt;程式碼範例&lt;/h1&gt;
&lt;h2 id="違反-srp三種職責擠在一個類別"&gt;違反 SRP：三種職責擠在一個類別&lt;/h2&gt;
&lt;p&gt;以下是一個很常見的反模式，在實際專案中幾乎隨處可見：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;ReportService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;private&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; _connectionString;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; ReportService(&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; connectionString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt; _connectionString = connectionString;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責一：取得業務資料（屬於業務邏輯層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：業務分析師、後端工程師&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; List&amp;lt;Order&amp;gt; GetOrders()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;using&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; conn = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; SqlConnection(_connectionString);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 查詢並回傳訂單清單...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; List&amp;lt;Order&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責二：格式化報表（屬於呈現層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：前端工程師、設計師&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; FormatAsHtml(List&amp;lt;Order&amp;gt; orders)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; sb = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; StringBuilder();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;&amp;#34;&amp;lt;table&amp;gt;&amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Total&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;&amp;lt;tbody&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;25&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;foreach&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; o &lt;span style="color:#ff7b72"&gt;in&lt;/span&gt; orders)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;26&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;$&amp;#34;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;{o.Id}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;{o.Total:C}&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;27&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;&amp;#34;&amp;lt;/tbody&amp;gt;&amp;lt;/table&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;28&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; sb.ToString();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;29&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;30&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;31&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責三：傳送通知（屬於基礎設施層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;32&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：維運團隊、產品經理&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;33&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; SendByEmail(&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; recipient, &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; content)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;34&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;35&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; client = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; SmtpClient(&lt;span style="color:#a5d6ff"&gt;&amp;#34;smtp.example.com&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;36&lt;/span&gt;&lt;span&gt; client.Send(&lt;span style="color:#a5d6ff"&gt;&amp;#34;noreply@example.com&amp;#34;&lt;/span&gt;, recipient, &lt;span style="color:#a5d6ff"&gt;&amp;#34;Weekly Report&amp;#34;&lt;/span&gt;, content);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;37&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;38&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個設計的問題不只是「感覺不太對」，而是會帶來非常具體的麻煩：&lt;/p&gt;</description></item><item><title>什麼是 SOLID？與 OOP 的根源</title><link>https://kaiadv.com/posts/20260529/</link><pubDate>Fri, 29 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260529/</guid><description>&lt;p&gt;許多工程師都有過類似的經驗：接手一份「能跑就好」的舊專案，或只是想改一個小功能，卻發現牽一髮動全身，改了 A 壞了 B，修了 B 又壞了 C；幾輪下來，程式碼愈來愈難以理解，沒有人敢輕易動它。&lt;/p&gt;
&lt;p&gt;這種現象有個名字，叫做程式碼腐化（Software Rot）；它的成因不是因為工程師不夠努力，也不是因為程式語言本身的限制，而是因為程式碼在設計上缺乏清晰的邊界與結構，隨著時間累積，複雜度像滾雪球一樣不斷擴大。&lt;/p&gt;
&lt;p&gt;程式碼腐化常見的三種症狀：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fragility（脆弱性）&lt;/strong&gt;：系統在意想不到的地方發生錯誤；改了一處邏輯之後，看似無關的另一個模組卻開始出錯，原因是它們之間存在隱藏的耦合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rigidity（僵化性）&lt;/strong&gt;：每一個改動都需要修改大量的地方，工程師必須追蹤一長串的連鎖反應，導致任何變更的成本都極高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immobility（不動性）&lt;/strong&gt;：某段邏輯明明可以在其他地方複用，卻因為它與太多其他元件糾纏在一起，根本無法單獨抽出來。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SOLID 就是為了系統性地對抗這三種症狀而誕生的。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="solid-的由來"&gt;SOLID 的由來&lt;/h1&gt;
&lt;p&gt;Robert C. Martin（人稱 Uncle Bob）是《Clean Code》與《Clean Architecture》的作者，也是敏捷宣言的共同簽署人之一，他在 2000 年代初期，將多年在軟體工程領域的觀察與實踐整理成文，提出了五個核心設計原則。&lt;/p&gt;
&lt;p&gt;這五個原則各自在過去幾十年間分散出現於不同的學術論文與工程討論中，Uncle Bob 的貢獻在於將它們系統化，並賦予一致的框架；之後 Michael Feathers（《Working Effectively with Legacy Code》的作者）發現這五個原則的英文首字母恰好組成「SOLID」，這個縮寫從此沿用至今。&lt;/p&gt;
&lt;p&gt;五個原則各自對應的英文全名與中文名稱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S&lt;/strong&gt; — Single-Responsibility Principle：單一職責原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O&lt;/strong&gt; — Open-Closed Principle：開放封閉原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L&lt;/strong&gt; — Liskov Substitution Principle：里氏替換原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; — Interface Segregation Principle：介面隔離原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt; — Dependency Inversion Principle：依賴反轉原則&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="oop-的四個基石以及它們的局限"&gt;OOP 的四個基石，以及它們的局限&lt;/h1&gt;
&lt;p&gt;物件導向程式設計（OOP）提供了四個強大的工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;封裝（Encapsulation）&lt;/strong&gt;：將資料與操作資料的方法包裝在一起，隱藏實作細節，只對外暴露必要的介面；這讓模組之間的邊界更清楚，也保護了資料不被任意存取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;繼承（Inheritance）&lt;/strong&gt;：讓子類別繼承父類別的屬性與行為，實現程式碼的複用；它也建立了「is-a」的型別關係，讓一個子類別的物件可以被當作父類別使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多型（Polymorphism）&lt;/strong&gt;：同一個介面或方法名稱，可以在不同的類別中有不同的實作；呼叫端只需要知道「這個物件能做什麼」，不需要知道「它是怎麼做的」，讓程式碼更具彈性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽象（Abstraction）&lt;/strong&gt;：透過介面（Interface）或抽象類別（Abstract Class），只定義行為的「輪廓」，而不指定實作，這讓高階的業務邏輯可以與低階的實作細節解耦。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而這四個工具只是手段，本身並不保證良好的設計；一個大量使用繼承、卻讓子類別行為不一致的系統，可能比不用繼承更糟糕；一個把所有邏輯封裝在同一個「萬能類別」裡的設計，只是用封装帶來更大的隱患；OOP 告訴你可以做什麼，SOLID 告訴你應該怎麼做。&lt;/p&gt;</description></item></channel></rss>