<?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>Knowledge Cards on Tarragon</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/</link><description>Recent content in Knowledge Cards 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/software-design/design-principles/knowledge-cards/index.xml" rel="self" type="application/rss+xml"/><item><title>Single Responsibility Principle（單一職責原則）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/</guid><description>&lt;p>單一職責原則（Single Responsibility Principle，SRP）的定義是：一個模組應該只有一個改變的理由。Robert C. Martin 把「理由」對到提出變更的人——一個模組的變更應該只來自一群緊密合作、代表單一業務職能的人，這樣一群人算一個理由。違反這條原則的模組，在修改紀錄上呈現成 &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 成對的 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>SRP 是 SOLID 五原則的第一條。Martin 在《Agile Software Development: Principles, Patterns, and Practices》（2002）把它寫成「一個類別應該只有一個改變的理由」；2014 年他在〈The Single Responsibility Principle〉一文說明「理由」指的是人，並把原則改寫成兩句方向相反的指示：&lt;/p>
&lt;blockquote>
&lt;p>Gather together the things that change for the same reasons. Separate those things that change for different reasons.&lt;/p>&lt;/blockquote>
&lt;p>前一句要求把因同一個理由改變的程式碼放在一起，後一句要求把因不同理由改變的程式碼分開。切分的人只執行後一句，會把同一個理由的程式碼拆散到很多檔案，於是那群人每次提出變更，修改的人都要同時改好幾處，這就是 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery&lt;/a>；只執行前一句的極端是把所有程式碼都放在一起，於是不同理由的變更擠在同一個模組，這就是 &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>。SRP 要求兩句同時滿足：因同一個理由改變的程式碼在同一個模組，因不同理由改變的程式碼在不同模組。在《Clean Architecture》（2017）裡，Martin 把定義收斂成「A module should be responsible to one, and only one, actor」，actor 指的是要求同一類變更的一群人。&lt;/p>
&lt;p>&lt;strong>理由的計數單位是改動的種類，提出的人用來辨認種類。&lt;/strong> 一個 actor 要求的是同一類改動，所以理由可以從兩邊辨認：從提出變更的人，或從他們要求的改動種類。兩邊對得上時（財務部門要求的都是薪資規則的改動）看哪一邊結果都一樣；對不上時以改動種類為準——同一個人轉達好幾個職能的需求時，他提出的改動屬於不同理由；不同的人提出同一類改動時，仍是同一個理由。&lt;/p>
&lt;p>Martin 在 2014 年那篇文章裡說明這條原則的源頭是 David Parnas 1972 年的論文〈On the Criteria To Be Used in Decomposing Systems into Modules〉與 Edsger Dijkstra 提出的 separation of concerns：模組的切法要依「什麼會一起變」決定，不依程式執行的步驟決定。他在《Agile Software Development》的單一職責那一章也寫明，這條原則就是 Tom DeMarco 與 Meilir Page-Jones 所說的 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">Cohesion（內聚）&lt;/a>，只是把判斷依據從功能上的相關換成讓模組改變的力量。&lt;/p>
&lt;p>&lt;strong>「只做一件事」是另一條原則。&lt;/strong> SRP 最常見的誤讀是「一個類別只做一件事」。Martin 在《Clean Architecture》第七章說確實有一條原則是這樣講的，那條原則的對象是函式：一個函式只做一件事，用在把大函式拆小的重構裡。那一條管的是函式內部的步驟，判斷依據是程式碼在做什麼；SRP 管的是模組的邊界，判斷依據是誰會要求它改變。兩條原則的單位與依據都不同，所以一個只做一件事的類別仍然可能有兩個改變的理由，一個做了好幾件事的類別也可能只有一個。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>Martin 的例子是一個 &lt;code>Employee&lt;/code> 類別，裡面有三個方法：&lt;code>calculatePay&lt;/code> 依財務部門的規則計算薪資，&lt;code>reportHours&lt;/code> 依營運部門的需要產生工時報表，&lt;code>save&lt;/code> 依技術團隊選定的方式寫進資料庫。三個方法服務三群人——財務長、營運長、技術長各自管轄的部門——所以這個類別有三個改變的理由。三個方法放在同一個類別，損害出現在它們共用程式碼的時候：Martin 在《Clean Architecture》裡舉的是 &lt;code>calculatePay&lt;/code> 與 &lt;code>reportHours&lt;/code> 共用一個計算正常工時的 &lt;code>regularHours&lt;/code> 方法：財務部門要求調整加班時數的算法，負責這次修改的工程師改了那段共用計算，營運部門的工時報表跟著變，而營運部門沒有提出任何變更。&lt;/p>
&lt;p>判斷一個模組有幾個理由，要看它的變更要求的是哪幾種改動、各由哪一個職能決定，不看它有幾個方法：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>修改紀錄裡有好幾個不相干的需求改同一個檔案&lt;/strong>（不相干指要求的改動屬於不同種類）：這是改變理由不只一個的間接量測。不同理由的修改由不同的人同時進行時，改動落在同一處就會合併衝突，沒有文字衝突時也要重跑測試確認沒有改壞對方；Martin 把合併列為違反單一職責的另一個症狀。用 git 歷史統計每個檔案被幾個不同的需求修改，再逐個 commit 歸類，做法在&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>：上面 &lt;code>Employee&lt;/code> 的情形。兩個需求互不相干，卻因為住在同一個模組而互相影響。&lt;/li>
&lt;li>&lt;strong>bug 修正與重構不另算一個理由&lt;/strong>。Martin 的說法是 bug 修正與重構是程式設計師的責任，不是程式的；歸類時，一個 bug 修正併進它所修的那段程式碼原本屬於的那一類。&lt;/li>
&lt;li>&lt;strong>所有需求都由同一個人提出時&lt;/strong>（例如所有票都由同一位 PM 開），看提出的人分不出理由，要看每個需求要求的是哪一種改動。以一個擋下跨工作目錄 commit 的守衛腳本為例：解析 git 命令語法的改動跟著 git 的寫法變，判斷其他代理人是否正在工作的改動跟著團隊的並行流程變，擋下時印給使用者的訊息措辭跟著使用者回饋變，這三種改動各是一個理由。&lt;/li>
&lt;/ul>
&lt;p>依改動種類計數理由的判斷也適用於文件：一份文件被好幾個不相干的主題修改，就有好幾個改變它的理由，做法在&lt;a href="https://tarrragon.github.io/blog/record/document-split-by-responsibility/" data-link-title="依職責拆分文字文件：修改紀錄的訊號、切法與拆完之後的引用" data-link-desc="規則文件、操作手冊與範本的拆分：從版本歷史讀出一份文件承擔幾種職責，依職責而非長度決定切法，原檔保留導覽表、同一條規則只定義一次，以及拆完之後更新引用與檢查新檔之間的指引缺口">依職責拆分文字文件&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>套用 SRP 的設計責任是讓每一群提出變更的人，各自只需要改一個地方，而且改的時候不會動到別人在乎的行為。執行的判斷問句是：列出一段時間內改過這個模組的需求（範圍要涵蓋幾輪需求，選法見&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>〈用 git 歷史量出熱點〉的「統計的時間範圍」一條），逐一寫出要求的是哪一種改動、由哪一個職能決定；改動種類有兩種以上，就有兩個以上的理由，可以考慮依理由切開。&lt;/p>
&lt;p>依技術層次切（驗證器、資料存取、寄信服務各一個類別）常常與依理由切重合，因為驗證規則、資料庫結構與信件內容通常由不同的人決定；兩種切法分岔時，以每個類別會被哪一個職能要求改變為準。切開之後反過來檢查：同一個理由的變更是否需要同時改好幾個新模組，是的話就切過頭了，要把那幾個新模組併回來。&lt;/p>
&lt;p>SRP 與 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">開放封閉原則&lt;/a>常一起使用：SRP 決定模組的邊界畫在哪裡，開放封閉原則決定模組裡哪些部分對外公開、讓其他模組不必修改它就能擴充，哪些留在內部。開放封閉原則在 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></description><content:encoded><![CDATA[<p>單一職責原則（Single Responsibility Principle，SRP）的定義是：一個模組應該只有一個改變的理由。Robert C. Martin 把「理由」對到提出變更的人——一個模組的變更應該只來自一群緊密合作、代表單一業務職能的人，這樣一群人算一個理由。違反這條原則的模組，在修改紀錄上呈現成 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）</a>：同一個模組因為不同的理由、以不同的方式反覆被修改；把同一個理由的程式碼拆散到好幾個模組，則是與 Divergent Change 成對的 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>SRP 是 SOLID 五原則的第一條。Martin 在《Agile Software Development: Principles, Patterns, and Practices》（2002）把它寫成「一個類別應該只有一個改變的理由」；2014 年他在〈The Single Responsibility Principle〉一文說明「理由」指的是人，並把原則改寫成兩句方向相反的指示：</p>
<blockquote>
<p>Gather together the things that change for the same reasons. Separate those things that change for different reasons.</p></blockquote>
<p>前一句要求把因同一個理由改變的程式碼放在一起，後一句要求把因不同理由改變的程式碼分開。切分的人只執行後一句，會把同一個理由的程式碼拆散到很多檔案，於是那群人每次提出變更，修改的人都要同時改好幾處，這就是 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery</a>；只執行前一句的極端是把所有程式碼都放在一起，於是不同理由的變更擠在同一個模組，這就是 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change</a>。SRP 要求兩句同時滿足：因同一個理由改變的程式碼在同一個模組，因不同理由改變的程式碼在不同模組。在《Clean Architecture》（2017）裡，Martin 把定義收斂成「A module should be responsible to one, and only one, actor」，actor 指的是要求同一類變更的一群人。</p>
<p><strong>理由的計數單位是改動的種類，提出的人用來辨認種類。</strong> 一個 actor 要求的是同一類改動，所以理由可以從兩邊辨認：從提出變更的人，或從他們要求的改動種類。兩邊對得上時（財務部門要求的都是薪資規則的改動）看哪一邊結果都一樣；對不上時以改動種類為準——同一個人轉達好幾個職能的需求時，他提出的改動屬於不同理由；不同的人提出同一類改動時，仍是同一個理由。</p>
<p>Martin 在 2014 年那篇文章裡說明這條原則的源頭是 David Parnas 1972 年的論文〈On the Criteria To Be Used in Decomposing Systems into Modules〉與 Edsger Dijkstra 提出的 separation of concerns：模組的切法要依「什麼會一起變」決定，不依程式執行的步驟決定。他在《Agile Software Development》的單一職責那一章也寫明，這條原則就是 Tom DeMarco 與 Meilir Page-Jones 所說的 <a href="/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">Cohesion（內聚）</a>，只是把判斷依據從功能上的相關換成讓模組改變的力量。</p>
<p><strong>「只做一件事」是另一條原則。</strong> SRP 最常見的誤讀是「一個類別只做一件事」。Martin 在《Clean Architecture》第七章說確實有一條原則是這樣講的，那條原則的對象是函式：一個函式只做一件事，用在把大函式拆小的重構裡。那一條管的是函式內部的步驟，判斷依據是程式碼在做什麼；SRP 管的是模組的邊界，判斷依據是誰會要求它改變。兩條原則的單位與依據都不同，所以一個只做一件事的類別仍然可能有兩個改變的理由，一個做了好幾件事的類別也可能只有一個。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>Martin 的例子是一個 <code>Employee</code> 類別，裡面有三個方法：<code>calculatePay</code> 依財務部門的規則計算薪資，<code>reportHours</code> 依營運部門的需要產生工時報表，<code>save</code> 依技術團隊選定的方式寫進資料庫。三個方法服務三群人——財務長、營運長、技術長各自管轄的部門——所以這個類別有三個改變的理由。三個方法放在同一個類別，損害出現在它們共用程式碼的時候：Martin 在《Clean Architecture》裡舉的是 <code>calculatePay</code> 與 <code>reportHours</code> 共用一個計算正常工時的 <code>regularHours</code> 方法：財務部門要求調整加班時數的算法，負責這次修改的工程師改了那段共用計算，營運部門的工時報表跟著變，而營運部門沒有提出任何變更。</p>
<p>判斷一個模組有幾個理由，要看它的變更要求的是哪幾種改動、各由哪一個職能決定，不看它有幾個方法：</p>
<ul>
<li><strong>修改紀錄裡有好幾個不相干的需求改同一個檔案</strong>（不相干指要求的改動屬於不同種類）：這是改變理由不只一個的間接量測。不同理由的修改由不同的人同時進行時，改動落在同一處就會合併衝突，沒有文字衝突時也要重跑測試確認沒有改壞對方；Martin 把合併列為違反單一職責的另一個症狀。用 git 歷史統計每個檔案被幾個不同的需求修改，再逐個 commit 歸類，做法在<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>：上面 <code>Employee</code> 的情形。兩個需求互不相干，卻因為住在同一個模組而互相影響。</li>
<li><strong>bug 修正與重構不另算一個理由</strong>。Martin 的說法是 bug 修正與重構是程式設計師的責任，不是程式的；歸類時，一個 bug 修正併進它所修的那段程式碼原本屬於的那一類。</li>
<li><strong>所有需求都由同一個人提出時</strong>（例如所有票都由同一位 PM 開），看提出的人分不出理由，要看每個需求要求的是哪一種改動。以一個擋下跨工作目錄 commit 的守衛腳本為例：解析 git 命令語法的改動跟著 git 的寫法變，判斷其他代理人是否正在工作的改動跟著團隊的並行流程變，擋下時印給使用者的訊息措辭跟著使用者回饋變，這三種改動各是一個理由。</li>
</ul>
<p>依改動種類計數理由的判斷也適用於文件：一份文件被好幾個不相干的主題修改，就有好幾個改變它的理由，做法在<a href="/blog/record/document-split-by-responsibility/" data-link-title="依職責拆分文字文件：修改紀錄的訊號、切法與拆完之後的引用" data-link-desc="規則文件、操作手冊與範本的拆分：從版本歷史讀出一份文件承擔幾種職責，依職責而非長度決定切法，原檔保留導覽表、同一條規則只定義一次，以及拆完之後更新引用與檢查新檔之間的指引缺口">依職責拆分文字文件</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>套用 SRP 的設計責任是讓每一群提出變更的人，各自只需要改一個地方，而且改的時候不會動到別人在乎的行為。執行的判斷問句是：列出一段時間內改過這個模組的需求（範圍要涵蓋幾輪需求，選法見<a href="/blog/software-design/design-principles/module-boundary-from-change-history/" data-link-title="從修改紀錄畫模組邊界：修改競爭、熱點的量測與分類、職責歸類、拆分時維持行為的做法、拆分時機與拆完的驗證" data-link-desc="用 git 歷史統計每個檔案被幾個不同的需求修改、把結果當成單一職責的間接量測，修改熱點的分類與處理，逐個 commit 把需求歸到職責以決定切法，拆分時與語言無關的做法（特徵測試、搬移與修改分開提交、單向依賴），以及拆分合併的時機與拆完之後的驗證；量測只讀版本歷史，與程式語言無關">從修改紀錄畫模組邊界</a>〈用 git 歷史量出熱點〉的「統計的時間範圍」一條），逐一寫出要求的是哪一種改動、由哪一個職能決定；改動種類有兩種以上，就有兩個以上的理由，可以考慮依理由切開。</p>
<p>依技術層次切（驗證器、資料存取、寄信服務各一個類別）常常與依理由切重合，因為驗證規則、資料庫結構與信件內容通常由不同的人決定；兩種切法分岔時，以每個類別會被哪一個職能要求改變為準。切開之後反過來檢查：同一個理由的變更是否需要同時改好幾個新模組，是的話就切過頭了，要把那幾個新模組併回來。</p>
<p>SRP 與 <a href="/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">開放封閉原則</a>常一起使用：SRP 決定模組的邊界畫在哪裡，開放封閉原則決定模組裡哪些部分對外公開、讓其他模組不必修改它就能擴充，哪些留在內部。開放封閉原則在 Python 的寫法與例子在<a href="/blog/programming/python/basic/00-philosophy/open-closed-principle/" data-link-title="開放封閉原則與認知負擔" data-link-desc="從認知負擔的視角重新理解 SOLID 原則">開放封閉原則</a>。</p>
]]></content:encoded></item><item><title>Divergent Change（發散式變更）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/divergent-change/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/divergent-change/</guid><description>&lt;p>Divergent Change（發散式變更，其他中文資料常寫作「發散式變化」）是 Martin Fowler 在《Refactoring》列出的程式碼壞味道：一個模組因為不同的理由、以不同的方式反覆被修改。它是 &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;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Divergent Change 與 Shotgun Surgery 描述的是同一組對應關係的兩種失衡：改變的理由與模組之間應該一對一。同一個理由的好幾個需求都改同一個模組是正常的&lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">內聚&lt;/a>；屬於不同理由的需求改同一個模組是 Divergent Change，同一個理由的一個需求要改好幾個模組是 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery&lt;/a>。&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="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則&lt;/a>的兩句指示各自對應一種：「把因不同理由改變的程式碼分開」修的是 Divergent Change，「把因同一個理由改變的程式碼放在一起」修的是 Shotgun Surgery。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>Fowler 給的辨識方式是：修改某個模組時，心裡想的是「加一種資料庫要改這三個函式，加一種金融商品要改那四個函式」——兩種變更各自集中在模組裡的幾個函式，卻共用同一個模組。可以觀察到的訊號有三種：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>修改紀錄&lt;/strong>：同一個檔案在一段時間內被好幾個不相干的需求修改。以需求編號統計每個檔案被幾個不同需求改過，再逐個 commit 歸類，做法在&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>：處理不相干需求的人反覆在同一個檔案上解合併衝突、或等對方合併之後重跑測試。&lt;/li>
&lt;li>&lt;strong>檔案內的分群&lt;/strong>：模組裡的函式可以分成幾群，每一群只被某一類需求修改，群與群之間很少一起改。&lt;/li>
&lt;/ul>
&lt;p>站內偵測規則（以方法分組數判斷）與 Extract Class 的操作範例在 &lt;a href="https://tarrragon.github.io/blog/record/code_smell/" data-link-title="Code Smell 檢查清單" data-link-desc="在這裡列出常見的 Code Smell 清單，提供 設計和派工做 review 的參考及早察覺問題">Code Smell 檢查清單&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>修正 Divergent Change 的方向是依改變的理由把模組切開：先找出每一類需求各自會改的那一群函式與資料，再用 Extract Class（拆出類別）或把模組改成套件、每一群移到一個子模組。切開之前先用測試鎖住現有行為，切開之後確認同一類需求的變更只需要改一個新模組；若同一類需求要同時改好幾個新模組，代表切過頭，產生了 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Divergent Change（發散式變更，其他中文資料常寫作「發散式變化」）是 Martin Fowler 在《Refactoring》列出的程式碼壞味道：一個模組因為不同的理由、以不同的方式反覆被修改。它是 <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> 被違反時在修改紀錄上留下的形態——模組有幾個改變的理由，就會被幾種不相干的需求輪流修改。與它方向相反的壞味道是 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Divergent Change 與 Shotgun Surgery 描述的是同一組對應關係的兩種失衡：改變的理由與模組之間應該一對一。同一個理由的好幾個需求都改同一個模組是正常的<a href="/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">內聚</a>；屬於不同理由的需求改同一個模組是 Divergent Change，同一個理由的一個需求要改好幾個模組是 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery</a>。<a href="/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/" data-link-title="Single Responsibility Principle（單一職責原則）" data-link-desc="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則</a>的兩句指示各自對應一種：「把因不同理由改變的程式碼分開」修的是 Divergent Change，「把因同一個理由改變的程式碼放在一起」修的是 Shotgun Surgery。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>Fowler 給的辨識方式是：修改某個模組時，心裡想的是「加一種資料庫要改這三個函式，加一種金融商品要改那四個函式」——兩種變更各自集中在模組裡的幾個函式，卻共用同一個模組。可以觀察到的訊號有三種：</p>
<ul>
<li><strong>修改紀錄</strong>：同一個檔案在一段時間內被好幾個不相干的需求修改。以需求編號統計每個檔案被幾個不同需求改過，再逐個 commit 歸類，做法在<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>：處理不相干需求的人反覆在同一個檔案上解合併衝突、或等對方合併之後重跑測試。</li>
<li><strong>檔案內的分群</strong>：模組裡的函式可以分成幾群，每一群只被某一類需求修改，群與群之間很少一起改。</li>
</ul>
<p>站內偵測規則（以方法分組數判斷）與 Extract Class 的操作範例在 <a href="/blog/record/code_smell/" data-link-title="Code Smell 檢查清單" data-link-desc="在這裡列出常見的 Code Smell 清單，提供 設計和派工做 review 的參考及早察覺問題">Code Smell 檢查清單</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>修正 Divergent Change 的方向是依改變的理由把模組切開：先找出每一類需求各自會改的那一群函式與資料，再用 Extract Class（拆出類別）或把模組改成套件、每一群移到一個子模組。切開之前先用測試鎖住現有行為，切開之後確認同一類需求的變更只需要改一個新模組；若同一類需求要同時改好幾個新模組，代表切過頭，產生了 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery</a>。</p>
]]></content:encoded></item><item><title>Shotgun Surgery（散彈式修改）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/</guid><description>&lt;p>Shotgun Surgery（散彈式修改，其他中文資料常寫作「霰彈式修改」或「散彈槍手術」）是 Martin Fowler 在《Refactoring》列出的程式碼壞味道：每做一種變更，都要在很多個模組裡各做一點小修改。它是 &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> 的反方向——那一種是多個不相干的需求改同一個模組，這一種是一個需求要改多個模組。依 &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>&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="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則&lt;/a>有兩句方向相反的指示：把因不同理由改變的程式碼分開，把因同一個理由改變的程式碼放在一起。只照前一句切，最常出現的結果就是 Shotgun Surgery——切的人看到「做了好幾件事」就切開，沒有檢查那幾件事是不是總是一起改。它與 &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> 都是改變的理由與模組沒有一對一對應的情形，修正方向相反，所以拆檔之後要兩個方向都檢查。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>Fowler 指出這種壞味道的主要風險是漏改：變更散在很多地方時，很難全部找到，漏掉一處就留下不一致。常見的形態有三種：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>依行數拆檔&lt;/strong>：把一個 900 行、只屬於同一個需求連續演進的解析器切成三份，切的時候不看修改落在哪裡，之後同一個主題的修改常常要同時改好幾個檔。行數判斷與不同需求數的判斷在這種檔案上給出相反的結論，對照在&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>：欄位定義、資料存取、序列化、畫面各在一個檔案，加一個欄位就要依序改過每一層。這種情形有時是分層架構刻意付的成本，判斷時看這幾層是不是總是一起改、而且由同一個理由驅動。&lt;/li>
&lt;li>&lt;strong>同一段規則複製在好幾處&lt;/strong>：規則改了，每一份複本都要找出來改。這與 DRY 處理的是同一件事，Python 的寫法在&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則&lt;/a>。&lt;/li>
&lt;/ul>
&lt;p>站內偵測規則與修正範例（Extract Interface、Introduce Facade）在 &lt;a href="https://tarrragon.github.io/blog/record/code_smell/" data-link-title="Code Smell 檢查清單" data-link-desc="在這裡列出常見的 Code Smell 清單，提供 設計和派工做 review 的參考及早察覺問題">Code Smell 檢查清單&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>修正 Shotgun Surgery 的方向是提高&lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">內聚&lt;/a>，把總是一起改的函式與資料收回同一個模組：用 Move Function、Move Field 把散落的函式與資料移到一處，散開的那幾個模組若只剩空殼就用 Inline Class 併回去。判斷問句是：最近幾次同一類變更，各改了哪幾個檔案；每次都改同一組檔案，那一組就是應該合併的候選。合併之後回頭檢查另一個方向：新的模組是否因此開始被不相干的需求修改，是的話就併過頭了，變成 &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>。&lt;/p></description><content:encoded><![CDATA[<p>Shotgun Surgery（散彈式修改，其他中文資料常寫作「霰彈式修改」或「散彈槍手術」）是 Martin Fowler 在《Refactoring》列出的程式碼壞味道：每做一種變更，都要在很多個模組裡各做一點小修改。它是 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）</a> 的反方向——那一種是多個不相干的需求改同一個模組，這一種是一個需求要改多個模組。依 <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><a href="/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/" data-link-title="Single Responsibility Principle（單一職責原則）" data-link-desc="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則</a>有兩句方向相反的指示：把因不同理由改變的程式碼分開，把因同一個理由改變的程式碼放在一起。只照前一句切，最常出現的結果就是 Shotgun Surgery——切的人看到「做了好幾件事」就切開，沒有檢查那幾件事是不是總是一起改。它與 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change</a> 都是改變的理由與模組沒有一對一對應的情形，修正方向相反，所以拆檔之後要兩個方向都檢查。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>Fowler 指出這種壞味道的主要風險是漏改：變更散在很多地方時，很難全部找到，漏掉一處就留下不一致。常見的形態有三種：</p>
<ul>
<li><strong>依行數拆檔</strong>：把一個 900 行、只屬於同一個需求連續演進的解析器切成三份，切的時候不看修改落在哪裡，之後同一個主題的修改常常要同時改好幾個檔。行數判斷與不同需求數的判斷在這種檔案上給出相反的結論，對照在<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>：欄位定義、資料存取、序列化、畫面各在一個檔案，加一個欄位就要依序改過每一層。這種情形有時是分層架構刻意付的成本，判斷時看這幾層是不是總是一起改、而且由同一個理由驅動。</li>
<li><strong>同一段規則複製在好幾處</strong>：規則改了，每一份複本都要找出來改。這與 DRY 處理的是同一件事，Python 的寫法在<a href="/blog/programming/python/basic/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則</a>。</li>
</ul>
<p>站內偵測規則與修正範例（Extract Interface、Introduce Facade）在 <a href="/blog/record/code_smell/" data-link-title="Code Smell 檢查清單" data-link-desc="在這裡列出常見的 Code Smell 清單，提供 設計和派工做 review 的參考及早察覺問題">Code Smell 檢查清單</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>修正 Shotgun Surgery 的方向是提高<a href="/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">內聚</a>，把總是一起改的函式與資料收回同一個模組：用 Move Function、Move Field 把散落的函式與資料移到一處，散開的那幾個模組若只剩空殼就用 Inline Class 併回去。判斷問句是：最近幾次同一類變更，各改了哪幾個檔案；每次都改同一組檔案，那一組就是應該合併的候選。合併之後回頭檢查另一個方向：新的模組是否因此開始被不相干的需求修改，是的話就併過頭了，變成 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change</a>。</p>
]]></content:encoded></item><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><item><title>DRY（Don't Repeat Yourself）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/dry-principle/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/dry-principle/</guid><description>&lt;p>DRY（Don&amp;rsquo;t Repeat Yourself）出自 Andy Hunt 與 Dave Thomas 1999 年的《The Pragmatic Programmer》：「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.」系統裡的每一份知識——一條業務規則、一個資料格式、一個設定值——只能有一個權威的表示。違反 DRY 的後果是 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）&lt;/a>：同一份知識的每一份複本都要找出來改，漏掉一處就不一致。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>DRY 管的對象是知識，不是程式碼的字面。2019 年的第二版特別說明，很多讀者把 DRY 讀成「不要複製貼上程式碼」，而 Hunt 與 Thomas 指的是知識與意圖的重複：同一件事寫在兩個地方，即使兩處的寫法完全不同（一段程式碼與描述它的文件、一份資料庫綱要與對應的資料結構），也是重複。&lt;/p>
&lt;p>反方向也成立：兩段字面相同的程式碼，如果因為不同的理由改變，就不是同一份知識。&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;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>。Martin 在《Clean Architecture》的單一職責那一章把這種字面相同、理由不同的情形稱為 accidental duplication（意外的重複）。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>需要套用 DRY 的訊號，是同一份知識的變更要同時改好幾處：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>同一條規則複製在好幾個檔案&lt;/strong>：例如四個腳本各自寫了一份相同的 git 指令包裝函式，要改一個 git 指令選項就得改四次。四份都因同一個理由改變（呼叫 git 的方式），是同一份知識，所以抽成一個共用函式、四處改成呼叫它。Python 專案的做法與遷移順序在 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫&lt;/a>。&lt;/li>
&lt;li>&lt;strong>同一個常數寫在好幾處&lt;/strong>：例如逾時秒數同時寫在程式碼、設定檔與文件裡。保留一個權威來源（通常是程式執行時實際讀取的那一處，例如設定檔），程式碼讀它、文件連到它。&lt;/li>
&lt;li>&lt;strong>程式碼與文件描述同一件事&lt;/strong>：文件抄了一份參數清單，程式碼改了而文件沒改。讓文件從程式碼產生，或文件只寫程式碼表達不了的部分。&lt;/li>
&lt;/ul>
&lt;p>判斷兩段相似的程式碼要不要合併時，Martin Fowler 在《Refactoring》引述 Don Roberts 的 Rule of Three：第一次照寫，第二次寫出相似的程式碼時先容忍重複，第三次才抽出共用的部分。Rule of Three 決定的是什麼時候判斷，要不要合併仍由下一節的判斷問句決定：等到有三份相似的程式碼，才累積得出足夠的修改紀錄，看得出它們是不是一起改。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>套用 DRY 的設計責任是讓每一份知識只有一個權威來源，而合併之前先確認那幾段程式碼是同一份知識。判斷問句是：這幾處過去的修改，是否每次都一起改、而且由同一個需求驅動；兩個條件都成立才是同一份知識，合併；任一個不成立，字面相同也分開保留。合併之後，函式裡開始需要一個參數來分支成兩套行為，代表那兩套行為各自由不同的理由改變，是合併錯了的訊號。&lt;/p></description><content:encoded><![CDATA[<p>DRY（Don&rsquo;t Repeat Yourself）出自 Andy Hunt 與 Dave Thomas 1999 年的《The Pragmatic Programmer》：「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.」系統裡的每一份知識——一條業務規則、一個資料格式、一個設定值——只能有一個權威的表示。違反 DRY 的後果是 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）</a>：同一份知識的每一份複本都要找出來改，漏掉一處就不一致。</p>
<h2 id="概念位置">概念位置</h2>
<p>DRY 管的對象是知識，不是程式碼的字面。2019 年的第二版特別說明，很多讀者把 DRY 讀成「不要複製貼上程式碼」，而 Hunt 與 Thomas 指的是知識與意圖的重複：同一件事寫在兩個地方，即使兩處的寫法完全不同（一段程式碼與描述它的文件、一份資料庫綱要與對應的資料結構），也是重複。</p>
<p>反方向也成立：兩段字面相同的程式碼，如果因為不同的理由改變，就不是同一份知識。<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>的判斷在這裡接手：兩段程式碼要求的改動屬於不同種類（一段跟著某個職能的規則變、另一段跟著另一個職能的規則變），現在相同只是碰巧，合併成一個共用函式之後，為其中一個需求做的修改會改到另一個需求依賴的行為，那個共用函式就成了 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）</a>。Martin 在《Clean Architecture》的單一職責那一章把這種字面相同、理由不同的情形稱為 accidental duplication（意外的重複）。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>需要套用 DRY 的訊號，是同一份知識的變更要同時改好幾處：</p>
<ul>
<li><strong>同一條規則複製在好幾個檔案</strong>：例如四個腳本各自寫了一份相同的 git 指令包裝函式，要改一個 git 指令選項就得改四次。四份都因同一個理由改變（呼叫 git 的方式），是同一份知識，所以抽成一個共用函式、四處改成呼叫它。Python 專案的做法與遷移順序在 <a href="/blog/programming/python/basic/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a>。</li>
<li><strong>同一個常數寫在好幾處</strong>：例如逾時秒數同時寫在程式碼、設定檔與文件裡。保留一個權威來源（通常是程式執行時實際讀取的那一處，例如設定檔），程式碼讀它、文件連到它。</li>
<li><strong>程式碼與文件描述同一件事</strong>：文件抄了一份參數清單，程式碼改了而文件沒改。讓文件從程式碼產生，或文件只寫程式碼表達不了的部分。</li>
</ul>
<p>判斷兩段相似的程式碼要不要合併時，Martin Fowler 在《Refactoring》引述 Don Roberts 的 Rule of Three：第一次照寫，第二次寫出相似的程式碼時先容忍重複，第三次才抽出共用的部分。Rule of Three 決定的是什麼時候判斷，要不要合併仍由下一節的判斷問句決定：等到有三份相似的程式碼，才累積得出足夠的修改紀錄，看得出它們是不是一起改。</p>
<h2 id="設計責任">設計責任</h2>
<p>套用 DRY 的設計責任是讓每一份知識只有一個權威來源，而合併之前先確認那幾段程式碼是同一份知識。判斷問句是：這幾處過去的修改，是否每次都一起改、而且由同一個需求驅動；兩個條件都成立才是同一份知識，合併；任一個不成立，字面相同也分開保留。合併之後，函式裡開始需要一個參數來分支成兩套行為，代表那兩套行為各自由不同的理由改變，是合併錯了的訊號。</p>
]]></content:encoded></item><item><title>Cohesion（內聚）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/</guid><description>&lt;p>Cohesion（內聚）是一個模組內部的元素彼此相關、應該放在一起的程度。這裡的模組泛指任何有對外介面的程式單位：函式、類別、檔案或套件。這個詞由 Larry Constantine 在 1960 年代後期的結構化設計（structured design）裡提出，1974 年 Stevens、Myers 與 Constantine 在 IBM Systems Journal 的〈Structured Design〉一文把它與 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/coupling/" data-link-title="Coupling（耦合）" data-link-desc="改一個模組總是連帶要改另一個、或要判斷兩個模組之間的依賴可不可以接受時：耦合是模組之間互相依賴的程度，依兩個模組共享什麼分成由強到弱的幾種">Coupling（耦合）&lt;/a> 一起寫成模組設計的兩個衡量標準：內聚量模組內部，耦合量模組之間。內聚與 &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>內聚依「這些元素為什麼放在一起」分成幾種。由弱到強的排序依據是放在一起的理由與模組那件工作的關係有多緊：偶然內聚的元素之間完全無關，功能內聚的每個元素都屬於同一件工作。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>偶然內聚（coincidental）&lt;/strong>：放在一起沒有任何理由，例如一個什麼都放的 &lt;code>utils&lt;/code> 模組。&lt;/li>
&lt;li>&lt;strong>邏輯內聚（logical）&lt;/strong>：元素屬於同一個類別，例如所有輸入處理都放在一起，而每一種輸入的處理彼此無關。&lt;/li>
&lt;li>&lt;strong>時間內聚（temporal）&lt;/strong>：元素在同一個時間點執行，例如程式啟動時要做的所有初始化。&lt;/li>
&lt;li>&lt;strong>程序內聚（procedural）&lt;/strong>：元素總是依某個順序執行。&lt;/li>
&lt;li>&lt;strong>通訊內聚（communicational）&lt;/strong>：元素操作同一份資料。&lt;/li>
&lt;li>&lt;strong>循序內聚（sequential）&lt;/strong>：一個元素的輸出是下一個元素的輸入。&lt;/li>
&lt;li>&lt;strong>功能內聚（functional）&lt;/strong>：所有元素一起完成一件定義明確的工作。&lt;/li>
&lt;/ul>
&lt;p>Robert C. Martin 在《Agile Software Development》的單一職責那一章寫明，這條原則在 Tom DeMarco 與 Meilir Page-Jones 的著作裡已有描述，「They called it cohesion」，他們把內聚定義成模組元素在功能上的相關程度；Martin 在那一章把內聚的意思稍微移動，改成和「讓模組改變的力量」相關。兩種判斷用到的資訊不同：功能相關看程式碼在做什麼，&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="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則&lt;/a>看誰會要求它改變。兩種判斷可能給出不同結果：一個模組完成一件定義明確的工作，屬於功能內聚；如果那件工作的規則由兩個業務職能各管一半，它仍然有兩個改變的理由。決定要不要拆時以改變的理由為準，內聚的層級用來描述元素放在一起的理由屬於哪一種。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>低內聚的模組有幾個可以觀察到的訊號：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>名字說不出它做什麼&lt;/strong>：&lt;code>utils&lt;/code>、&lt;code>helpers&lt;/code>、&lt;code>common&lt;/code>、&lt;code>misc&lt;/code> 這類名字通常代表偶然內聚，裡面的函式彼此沒有關係，只是沒有更好的去處。&lt;/li>
&lt;li>&lt;strong>改一處要讀整個模組，而讀到的大部分與這次修改無關&lt;/strong>：模組裡混了幾群互不相關的元素。&lt;/li>
&lt;li>&lt;strong>修改紀錄裡同一個模組被不相干的需求輪流修改&lt;/strong>：這是內聚不足在版本歷史上的樣子，也就是 &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>。&lt;/li>
&lt;/ul>
&lt;p>反方向的錯誤是把同一件工作拆到好幾個模組，每個模組單獨看都很小，而那件工作的每一次修改都要同時改它們，這是 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>提高內聚的設計責任是讓模組裡的每個元素都因為同一個理由放在這裡，理由指的是改變的理由：它會因為哪一種改動而改變。判斷問句是：逐一寫出模組裡每個元素會被哪一種改動要求修改（那一類改動由哪一個職能決定）；同一種改動的那一群留下，屬於別種改動的、或寫得出的原因只有「沒有別的地方放」的，移到它所屬的模組。移完之後回頭看 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/coupling/" data-link-title="Coupling（耦合）" data-link-desc="改一個模組總是連帶要改另一個、或要判斷兩個模組之間的依賴可不可以接受時：耦合是模組之間互相依賴的程度，依兩個模組共享什麼分成由強到弱的幾種">Coupling（耦合）&lt;/a>：移出去的元素如果仍然大量讀寫原模組的內部資料、而且每次修改都要和原模組一起改，那個元素與原模組本來就屬於同一個模組，移出去的判斷錯了。&lt;/p></description><content:encoded><![CDATA[<p>Cohesion（內聚）是一個模組內部的元素彼此相關、應該放在一起的程度。這裡的模組泛指任何有對外介面的程式單位：函式、類別、檔案或套件。這個詞由 Larry Constantine 在 1960 年代後期的結構化設計（structured design）裡提出，1974 年 Stevens、Myers 與 Constantine 在 IBM Systems Journal 的〈Structured Design〉一文把它與 <a href="/blog/software-design/design-principles/knowledge-cards/coupling/" data-link-title="Coupling（耦合）" data-link-desc="改一個模組總是連帶要改另一個、或要判斷兩個模組之間的依賴可不可以接受時：耦合是模組之間互相依賴的程度，依兩個模組共享什麼分成由強到弱的幾種">Coupling（耦合）</a> 一起寫成模組設計的兩個衡量標準：內聚量模組內部，耦合量模組之間。內聚與 <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>內聚依「這些元素為什麼放在一起」分成幾種。由弱到強的排序依據是放在一起的理由與模組那件工作的關係有多緊：偶然內聚的元素之間完全無關，功能內聚的每個元素都屬於同一件工作。</p>
<ul>
<li><strong>偶然內聚（coincidental）</strong>：放在一起沒有任何理由，例如一個什麼都放的 <code>utils</code> 模組。</li>
<li><strong>邏輯內聚（logical）</strong>：元素屬於同一個類別，例如所有輸入處理都放在一起，而每一種輸入的處理彼此無關。</li>
<li><strong>時間內聚（temporal）</strong>：元素在同一個時間點執行，例如程式啟動時要做的所有初始化。</li>
<li><strong>程序內聚（procedural）</strong>：元素總是依某個順序執行。</li>
<li><strong>通訊內聚（communicational）</strong>：元素操作同一份資料。</li>
<li><strong>循序內聚（sequential）</strong>：一個元素的輸出是下一個元素的輸入。</li>
<li><strong>功能內聚（functional）</strong>：所有元素一起完成一件定義明確的工作。</li>
</ul>
<p>Robert C. Martin 在《Agile Software Development》的單一職責那一章寫明，這條原則在 Tom DeMarco 與 Meilir Page-Jones 的著作裡已有描述，「They called it cohesion」，他們把內聚定義成模組元素在功能上的相關程度；Martin 在那一章把內聚的意思稍微移動，改成和「讓模組改變的力量」相關。兩種判斷用到的資訊不同：功能相關看程式碼在做什麼，<a href="/blog/software-design/design-principles/knowledge-cards/single-responsibility-principle/" data-link-title="Single Responsibility Principle（單一職責原則）" data-link-desc="要判斷一個模組、類別或文件該不該拆，或讀到「一個類別只做一件事」這種說法時：單一職責的定義是改變的理由，理由以改動的種類計數，提出變更的人用來辨認種類">單一職責原則</a>看誰會要求它改變。兩種判斷可能給出不同結果：一個模組完成一件定義明確的工作，屬於功能內聚；如果那件工作的規則由兩個業務職能各管一半，它仍然有兩個改變的理由。決定要不要拆時以改變的理由為準，內聚的層級用來描述元素放在一起的理由屬於哪一種。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>低內聚的模組有幾個可以觀察到的訊號：</p>
<ul>
<li><strong>名字說不出它做什麼</strong>：<code>utils</code>、<code>helpers</code>、<code>common</code>、<code>misc</code> 這類名字通常代表偶然內聚，裡面的函式彼此沒有關係，只是沒有更好的去處。</li>
<li><strong>改一處要讀整個模組，而讀到的大部分與這次修改無關</strong>：模組裡混了幾群互不相關的元素。</li>
<li><strong>修改紀錄裡同一個模組被不相干的需求輪流修改</strong>：這是內聚不足在版本歷史上的樣子，也就是 <a href="/blog/software-design/design-principles/knowledge-cards/divergent-change/" data-link-title="Divergent Change（發散式變更）" data-link-desc="同一個模組總是因為不相干的需求被修改、或並行開發的人一直在同一個檔案上等待彼此時：這是違反單一職責原則在修改紀錄上的樣子">Divergent Change（發散式變更）</a>。</li>
</ul>
<p>反方向的錯誤是把同一件工作拆到好幾個模組，每個模組單獨看都很小，而那件工作的每一次修改都要同時改它們，這是 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>提高內聚的設計責任是讓模組裡的每個元素都因為同一個理由放在這裡，理由指的是改變的理由：它會因為哪一種改動而改變。判斷問句是：逐一寫出模組裡每個元素會被哪一種改動要求修改（那一類改動由哪一個職能決定）；同一種改動的那一群留下，屬於別種改動的、或寫得出的原因只有「沒有別的地方放」的，移到它所屬的模組。移完之後回頭看 <a href="/blog/software-design/design-principles/knowledge-cards/coupling/" data-link-title="Coupling（耦合）" data-link-desc="改一個模組總是連帶要改另一個、或要判斷兩個模組之間的依賴可不可以接受時：耦合是模組之間互相依賴的程度，依兩個模組共享什麼分成由強到弱的幾種">Coupling（耦合）</a>：移出去的元素如果仍然大量讀寫原模組的內部資料、而且每次修改都要和原模組一起改，那個元素與原模組本來就屬於同一個模組，移出去的判斷錯了。</p>
]]></content:encoded></item><item><title>Coupling（耦合）</title><link>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/coupling/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/coupling/</guid><description>&lt;p>Coupling（耦合）是模組之間互相依賴的程度：一個模組改變時，另一個模組要跟著改的可能性與範圍。這裡的模組泛指任何有對外介面的程式單位：函式、類別、檔案或套件。這個詞與 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">Cohesion（內聚）&lt;/a> 一起由 Larry Constantine 在 1960 年代後期的結構化設計裡提出，1974 年 Stevens、Myers 與 Constantine 的〈Structured Design〉把它們寫成模組設計的兩個衡量標準：內聚量模組內部，耦合量模組之間。低耦合與高內聚常一起出現，原因是它們來自同一個切法：把會一起改的元素放進同一個模組，元素之間的讀寫就留在模組內部，模組之間要交換的資料與呼叫隨之變少。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>耦合依兩個模組共享的內容分成幾種，由強到弱是：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>內容耦合（content）&lt;/strong>：一個模組直接使用另一個模組內部的程式碼或資料，例如跳進對方的內部、改對方的私有欄位。&lt;/li>
&lt;li>&lt;strong>共用耦合（common）&lt;/strong>：好幾個模組存取同一份全域資料。&lt;/li>
&lt;li>&lt;strong>外部耦合（external）&lt;/strong>：模組共用一個外部規定的資料格式、通訊協定或裝置介面。&lt;/li>
&lt;li>&lt;strong>控制耦合（control）&lt;/strong>：一個模組傳一個旗標給另一個模組，由旗標決定接收的模組要走哪一條邏輯。&lt;/li>
&lt;li>&lt;strong>戳記耦合（stamp，其他中文資料也寫作「標記耦合」）&lt;/strong>：模組之間傳遞一整個複合資料結構，而接收的一方只用到其中一部分欄位。&lt;/li>
&lt;li>&lt;strong>資料耦合（data）&lt;/strong>：模組之間只透過參數傳遞需要的資料。&lt;/li>
&lt;/ul>
&lt;p>耦合越強，一個模組的修改越可能要求另一個模組跟著改。&lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">Open-Closed Principle（開放封閉原則）&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>：它們共用同一份全域資料，是共用耦合。Martin 對全域變數的說明在 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">Open-Closed Principle&lt;/a> 卡片。&lt;/li>
&lt;li>&lt;strong>函式收一個 &lt;code>mode&lt;/code> 或 &lt;code>is_xxx&lt;/code> 參數，依它切換成完全不同的兩套行為&lt;/strong>：呼叫端在控制被呼叫端的流程，是控制耦合；這種函式通常也代表兩套行為各自有不同的改變理由，拆成兩個函式之後，呼叫端直接呼叫它要的那一個。&lt;/li>
&lt;li>&lt;strong>函式收一整個物件、只讀其中一兩個欄位&lt;/strong>：例如計算運費的函式收整個訂單物件，只讀了寄送地址。訂單物件的結構一改，運費函式就要重新確認，是戳記耦合；測試這個函式時也得先造出一整個訂單物件。改成只收寄送地址，就降成資料耦合。&lt;/li>
&lt;/ul>
&lt;p>兩個模組之間有耦合本身不是錯誤，模組要合作就一定要交換資料或呼叫對方。要確認的是交換的內容是否只有完成這次呼叫實際用到的資料。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>降低耦合的設計責任是讓模組之間只交換合作需要的資料，而且依賴的方向穩定。判斷問句是：改這個模組時，有哪些別的模組要跟著改，原因是什麼；原因是那些模組讀了這個模組的內部、共用了同一份可變資料、或用旗標控制這個模組的流程，就把那份依賴改成更弱的形式，依情形選一種：只傳它實際用到的資料（降成資料耦合），或改成依賴一個抽象（做法見開放封閉原則）。降低耦合之前先確認那兩個模組是否本來就該是同一個：兩者過去的修改幾乎每次都一起發生，或一方大量讀寫另一方的內部資料，拆成兩個模組只會讓每次修改都要改兩處，形成 &lt;a href="https://tarrragon.github.io/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）&lt;/a>。這時把兩者合併成一個模組比降低它們之間的耦合更合適，合併之後它們放在一起的理由相同，內聚也隨之提高。&lt;/p></description><content:encoded><![CDATA[<p>Coupling（耦合）是模組之間互相依賴的程度：一個模組改變時，另一個模組要跟著改的可能性與範圍。這裡的模組泛指任何有對外介面的程式單位：函式、類別、檔案或套件。這個詞與 <a href="/blog/software-design/design-principles/knowledge-cards/cohesion/" data-link-title="Cohesion（內聚）" data-link-desc="要判斷一個模組裡的元素放在一起的理由夠不夠強、或讀到「高內聚、低耦合」想知道內聚量的是什麼時：內聚是模組內部元素彼此相關的程度，依放在一起的理由分成由弱到強的幾種">Cohesion（內聚）</a> 一起由 Larry Constantine 在 1960 年代後期的結構化設計裡提出，1974 年 Stevens、Myers 與 Constantine 的〈Structured Design〉把它們寫成模組設計的兩個衡量標準：內聚量模組內部，耦合量模組之間。低耦合與高內聚常一起出現，原因是它們來自同一個切法：把會一起改的元素放進同一個模組，元素之間的讀寫就留在模組內部，模組之間要交換的資料與呼叫隨之變少。</p>
<h2 id="概念位置">概念位置</h2>
<p>耦合依兩個模組共享的內容分成幾種，由強到弱是：</p>
<ul>
<li><strong>內容耦合（content）</strong>：一個模組直接使用另一個模組內部的程式碼或資料，例如跳進對方的內部、改對方的私有欄位。</li>
<li><strong>共用耦合（common）</strong>：好幾個模組存取同一份全域資料。</li>
<li><strong>外部耦合（external）</strong>：模組共用一個外部規定的資料格式、通訊協定或裝置介面。</li>
<li><strong>控制耦合（control）</strong>：一個模組傳一個旗標給另一個模組，由旗標決定接收的模組要走哪一條邏輯。</li>
<li><strong>戳記耦合（stamp，其他中文資料也寫作「標記耦合」）</strong>：模組之間傳遞一整個複合資料結構，而接收的一方只用到其中一部分欄位。</li>
<li><strong>資料耦合（data）</strong>：模組之間只透過參數傳遞需要的資料。</li>
</ul>
<p>耦合越強，一個模組的修改越可能要求另一個模組跟著改。<a href="/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">Open-Closed Principle（開放封閉原則）</a>用來降低耦合的手段是讓使用端只依賴一個固定的抽象：使用端與各個實作之間只剩下對那個抽象的依賴，實作的內部改變不會傳到使用端。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>耦合過強的訊號，是修改一個模組時發現要同時改另一個模組，而這次修改的需求只跟被修改的那個模組有關：</p>
<ul>
<li><strong>改一個模組的內部欄位，另一個模組編譯失敗或行為改變</strong>：另一個模組直接讀寫了這些欄位，是內容耦合。</li>
<li><strong>改一份全域設定，幾個看似無關的模組行為都變了</strong>：它們共用同一份全域資料，是共用耦合。Martin 對全域變數的說明在 <a href="/blog/software-design/design-principles/knowledge-cards/open-closed-principle/" data-link-title="Open-Closed Principle（開放封閉原則）" data-link-desc="要在新增一種格式、一種類型或一種規則時不必改動既有程式碼，或要判斷一個擴充點值不值得先做時：模組對選定的那一類變更封閉，靠的是使用端只依賴一個固定的抽象">Open-Closed Principle</a> 卡片。</li>
<li><strong>函式收一個 <code>mode</code> 或 <code>is_xxx</code> 參數，依它切換成完全不同的兩套行為</strong>：呼叫端在控制被呼叫端的流程，是控制耦合；這種函式通常也代表兩套行為各自有不同的改變理由，拆成兩個函式之後，呼叫端直接呼叫它要的那一個。</li>
<li><strong>函式收一整個物件、只讀其中一兩個欄位</strong>：例如計算運費的函式收整個訂單物件，只讀了寄送地址。訂單物件的結構一改，運費函式就要重新確認，是戳記耦合；測試這個函式時也得先造出一整個訂單物件。改成只收寄送地址，就降成資料耦合。</li>
</ul>
<p>兩個模組之間有耦合本身不是錯誤，模組要合作就一定要交換資料或呼叫對方。要確認的是交換的內容是否只有完成這次呼叫實際用到的資料。</p>
<h2 id="設計責任">設計責任</h2>
<p>降低耦合的設計責任是讓模組之間只交換合作需要的資料，而且依賴的方向穩定。判斷問句是：改這個模組時，有哪些別的模組要跟著改，原因是什麼；原因是那些模組讀了這個模組的內部、共用了同一份可變資料、或用旗標控制這個模組的流程，就把那份依賴改成更弱的形式，依情形選一種：只傳它實際用到的資料（降成資料耦合），或改成依賴一個抽象（做法見開放封閉原則）。降低耦合之前先確認那兩個模組是否本來就該是同一個：兩者過去的修改幾乎每次都一起發生，或一方大量讀寫另一方的內部資料，拆成兩個模組只會讓每次修改都要改兩處，形成 <a href="/blog/software-design/design-principles/knowledge-cards/shotgun-surgery/" data-link-title="Shotgun Surgery（散彈式修改）" data-link-desc="每做一個小變更都要同時改好幾個檔案、漏改一處就出錯，或依行數拆完檔之後同一個主題的修改散到好幾份時：這是同一個改變理由被拆散到多個模組的壞味道">Shotgun Surgery（散彈式修改）</a>。這時把兩者合併成一個模組比降低它們之間的耦合更合適，合併之後它們放在一起的理由相同，內聚也隨之提高。</p>
]]></content:encoded></item></channel></rss>