<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ocp on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ocp/</link><description>Recent content in Ocp on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 19 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ocp/index.xml" rel="self" type="application/rss+xml"/><item><title>Open-Closed Principle（開放封閉原則）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/open-closed-principle/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/open-closed-principle/</guid><description>&lt;p>開放封閉原則（Open-Closed Principle，OCP）的定義是：軟體實體（類別、模組、函式）應該對擴充開放、對修改封閉——需求改變時，靠新增程式碼擴充模組的行為，不改動已經能運作的程式碼。封閉有範圍：模組只對設計者選定的那幾類變更封閉，下一節說明為什麼不可能對所有變更封閉。這條原則由 Bertrand Meyer 在 1988 年的《Object-Oriented Software Construction》提出。它與 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/" data-link-title="Single Responsibility Principle（單一職責原則）" data-link-desc="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">Single Responsibility Principle（單一職責原則）&lt;/a> 常一起使用：單一職責原則決定模組的邊界畫在哪裡，開放封閉原則決定其中哪一類變更要做成擴充點。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>對擴充開放與對修改封閉看起來互相矛盾：要讓模組有新的行為，通常就得改它。Robert C. Martin 在 1996 年的〈The Open-Closed Principle〉裡給的解法是「抽象是關鍵」：使用端只依賴一個固定的抽象（例如一個介面或抽象類別），新的行為由新的實作提供。使用端依賴的抽象不變，所以使用端不必修改；實作可以一直增加，所以行為可以擴充。用單一職責原則的語言說，單一職責原則畫出模組的邊界，抽象是這條邊界上對外公開的部分，各個實作留在邊界內，其他模組不需要知道。&lt;/p>
&lt;p>同一篇文章另有一段〈Strategic Closure〉，是這條原則的使用條件：沒有一個有規模的程式能百分之百封閉，不論模組多「封閉」，總有某一類變更是它擋不住的。Martin 的例子是一個依序畫出所有圖形的函式，它對「新增一種圖形」封閉，對「先畫完所有圓形再畫方形」這種順序規則的變更就不封閉。所以封閉要有選擇：設計者要挑出要封閉的那幾類變更，Martin 給的依據是對使用者與產業的了解，判斷哪幾類變更最可能發生。&lt;/p>
&lt;p>「封閉要有選擇」這個條件讓開放封閉原則與 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）&lt;/a> 接在一起：修改紀錄是「哪幾類變更最可能發生」最直接的證據。一類變更在紀錄裡已經反覆發生、每次都要改同一個模組，就是 Divergent Change 的形態，也是值得封閉的候選。替紀錄裡從沒出現過的那一類變更預先做擴充點，現在就要多寫並維護一層抽象，換到的是一個未必發生的變更變便宜；這是 YAGNI 處理的那種取捨，它在什麼條件下成立見 &lt;a href="https://tarrragon.github.io/blog/record/yagni-boundary-three-axes/" data-link-title="YAGNI 的真實適用條件：成本對稱性、可逆性、領域先驗劃出邊界" data-link-desc="YAGNI 不是「永遠選最受限選項」、是「不為未來投入額外成本」的原則。用成本對稱性、可逆性、領域先驗三軸框架釐清「該選通用 default」與「該避免過度設計」的邊界、並補上 review checklist、架構規範、領域先驗清單三層制度補強。">YAGNI 的真實適用條件&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>需要開放封閉原則的訊號，是同一類變更反覆要求修改同一段程式碼：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>依類型分支的函式&lt;/strong>：一個 &lt;code>if&lt;/code> 或 &lt;code>switch&lt;/code> 依格式（PDF、Excel、HTML）或類型分派，每新增一種格式就要在這段程式碼裡多加一個分支，並重新確認沒有改壞其他分支。改成每種格式各一個實作、使用端只依賴共同的抽象之後，新增格式變成新增一個實作。&lt;/li>
&lt;li>&lt;strong>集中登記檔&lt;/strong>：新增一種實作之後，還要回到一份集中的清單登記它。這份清單仍然對「新增實作」不封閉，並行的需求也會在它的尾端同時追加而衝突（處置見&lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/module-boundary-from-change-history/" data-link-title="從修改紀錄畫模組邊界：修改競爭、熱點的量測與分類、職責歸類、拆分時維持行為的做法、拆分時機與拆完的驗證" data-link-desc="用 git 歷史統計每個檔案被幾個不同的需求修改、把結果當成單一職責的間接量測，修改熱點的分類與處理，逐個 commit 把需求歸到職責以決定切法，拆分時與語言無關的做法（特徵測試、搬移與修改分開提交、單向依賴），以及拆分合併的時機與拆完之後的驗證；量測只讀版本歷史，與程式語言無關">從修改紀錄畫模組邊界&lt;/a>〈熱點的分類與處理方式〉）；用自動登記（依命名規則或目錄掃描）可以讓這份清單也不必再改，代價是登記關係不再寫在一個看得到的地方。&lt;/li>
&lt;li>&lt;strong>共用的全域狀態&lt;/strong>：Martin 在同一篇文章裡指出，依賴全域變數的模組，對任何可能改寫那個變數的模組都無法封閉；依賴那個全域變數的模組很少、或那個全域變數不可能被不一致地使用時，損害才小。&lt;/li>
&lt;/ul>
&lt;p>各語言的寫法不同：Python 用抽象基類或 Protocol，Go 用 interface，Dart 用 abstract class。Python 的例子在&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/00-philosophy/open-closed-principle/" data-link-title="開放封閉原則與認知負擔" data-link-desc="從認知負擔的視角重新理解 SOLID 原則">開放封閉原則與認知負擔&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>套用開放封閉原則的設計責任是選對要封閉的變更。判斷問句是：這個擴充點要擋的那一類變更，在過去的修改紀錄裡發生過幾次，三種結果各有處置：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>已經反覆發生&lt;/strong>（紀錄裡有兩次以上、每次都改同一處）：做擴充點。&lt;/li>
&lt;li>&lt;strong>只發生過一次&lt;/strong>：看對使用者與產業的了解判斷它會不會再發生——例如報表格式由客戶要求、而客戶還會再要新格式——判斷會再發生才做；判斷不出來就先照原樣改，等它第二次發生。&lt;/li>
&lt;li>&lt;strong>從沒發生過&lt;/strong>：先不做。&lt;/li>
&lt;/ul>
&lt;p>擴充點做出來之後，抽象本身成了很多實作共同依賴的對象，修改抽象的成本隨實作數增加，所以抽象只放那一類變更裡每個實作都需要的操作，不放眼前某一個實作才有的細節。&lt;/p></description><content:encoded><![CDATA[<p>開放封閉原則（Open-Closed Principle，OCP）的定義是：軟體實體（類別、模組、函式）應該對擴充開放、對修改封閉——需求改變時，靠新增程式碼擴充模組的行為，不改動已經能運作的程式碼。封閉有範圍：模組只對設計者選定的那幾類變更封閉，下一節說明為什麼不可能對所有變更封閉。這條原則由 Bertrand Meyer 在 1988 年的《Object-Oriented Software Construction》提出。它與 <a href="/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/" data-link-title="Single Responsibility Principle（單一職責原則）" data-link-desc="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">Single Responsibility Principle（單一職責原則）</a> 常一起使用：單一職責原則決定模組的邊界畫在哪裡，開放封閉原則決定其中哪一類變更要做成擴充點。</p>
<h2 id="概念位置">概念位置</h2>
<p>對擴充開放與對修改封閉看起來互相矛盾：要讓模組有新的行為，通常就得改它。Robert C. Martin 在 1996 年的〈The Open-Closed Principle〉裡給的解法是「抽象是關鍵」：使用端只依賴一個固定的抽象（例如一個介面或抽象類別），新的行為由新的實作提供。使用端依賴的抽象不變，所以使用端不必修改；實作可以一直增加，所以行為可以擴充。用單一職責原則的語言說，單一職責原則畫出模組的邊界，抽象是這條邊界上對外公開的部分，各個實作留在邊界內，其他模組不需要知道。</p>
<p>同一篇文章另有一段〈Strategic Closure〉，是這條原則的使用條件：沒有一個有規模的程式能百分之百封閉，不論模組多「封閉」，總有某一類變更是它擋不住的。Martin 的例子是一個依序畫出所有圖形的函式，它對「新增一種圖形」封閉，對「先畫完所有圓形再畫方形」這種順序規則的變更就不封閉。所以封閉要有選擇：設計者要挑出要封閉的那幾類變更，Martin 給的依據是對使用者與產業的了解，判斷哪幾類變更最可能發生。</p>
<p>「封閉要有選擇」這個條件讓開放封閉原則與 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）</a> 接在一起：修改紀錄是「哪幾類變更最可能發生」最直接的證據。一類變更在紀錄裡已經反覆發生、每次都要改同一個模組，就是 Divergent Change 的形態，也是值得封閉的候選。替紀錄裡從沒出現過的那一類變更預先做擴充點，現在就要多寫並維護一層抽象，換到的是一個未必發生的變更變便宜；這是 YAGNI 處理的那種取捨，它在什麼條件下成立見 <a href="/blog/record/yagni-boundary-three-axes/" data-link-title="YAGNI 的真實適用條件：成本對稱性、可逆性、領域先驗劃出邊界" data-link-desc="YAGNI 不是「永遠選最受限選項」、是「不為未來投入額外成本」的原則。用成本對稱性、可逆性、領域先驗三軸框架釐清「該選通用 default」與「該避免過度設計」的邊界、並補上 review checklist、架構規範、領域先驗清單三層制度補強。">YAGNI 的真實適用條件</a>。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>需要開放封閉原則的訊號，是同一類變更反覆要求修改同一段程式碼：</p>
<ul>
<li><strong>依類型分支的函式</strong>：一個 <code>if</code> 或 <code>switch</code> 依格式（PDF、Excel、HTML）或類型分派，每新增一種格式就要在這段程式碼裡多加一個分支，並重新確認沒有改壞其他分支。改成每種格式各一個實作、使用端只依賴共同的抽象之後，新增格式變成新增一個實作。</li>
<li><strong>集中登記檔</strong>：新增一種實作之後，還要回到一份集中的清單登記它。這份清單仍然對「新增實作」不封閉，並行的需求也會在它的尾端同時追加而衝突（處置見<a href="/blog/software-design/design-principles/module-boundary-from-change-history/" data-link-title="從修改紀錄畫模組邊界：修改競爭、熱點的量測與分類、職責歸類、拆分時維持行為的做法、拆分時機與拆完的驗證" data-link-desc="用 git 歷史統計每個檔案被幾個不同的需求修改、把結果當成單一職責的間接量測，修改熱點的分類與處理，逐個 commit 把需求歸到職責以決定切法，拆分時與語言無關的做法（特徵測試、搬移與修改分開提交、單向依賴），以及拆分合併的時機與拆完之後的驗證；量測只讀版本歷史，與程式語言無關">從修改紀錄畫模組邊界</a>〈熱點的分類與處理方式〉）；用自動登記（依命名規則或目錄掃描）可以讓這份清單也不必再改，代價是登記關係不再寫在一個看得到的地方。</li>
<li><strong>共用的全域狀態</strong>：Martin 在同一篇文章裡指出，依賴全域變數的模組，對任何可能改寫那個變數的模組都無法封閉；依賴那個全域變數的模組很少、或那個全域變數不可能被不一致地使用時，損害才小。</li>
</ul>
<p>各語言的寫法不同：Python 用抽象基類或 Protocol，Go 用 interface，Dart 用 abstract class。Python 的例子在<a href="/blog/programming/python/basic/00-philosophy/open-closed-principle/" data-link-title="開放封閉原則與認知負擔" data-link-desc="從認知負擔的視角重新理解 SOLID 原則">開放封閉原則與認知負擔</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>套用開放封閉原則的設計責任是選對要封閉的變更。判斷問句是：這個擴充點要擋的那一類變更，在過去的修改紀錄裡發生過幾次，三種結果各有處置：</p>
<ul>
<li><strong>已經反覆發生</strong>（紀錄裡有兩次以上、每次都改同一處）：做擴充點。</li>
<li><strong>只發生過一次</strong>：看對使用者與產業的了解判斷它會不會再發生——例如報表格式由客戶要求、而客戶還會再要新格式——判斷會再發生才做；判斷不出來就先照原樣改，等它第二次發生。</li>
<li><strong>從沒發生過</strong>：先不做。</li>
</ul>
<p>擴充點做出來之後，抽象本身成了很多實作共同依賴的對象，修改抽象的成本隨實作數增加，所以抽象只放那一類變更裡每個實作都需要的操作，不放眼前某一個實作才有的細節。</p>
]]></content:encoded></item></channel></rss>