一個類別應該只有一個被變更的理由。職責=變更的軸線;如果你能想出一個以上的理由去修改一個類別,它就承擔了一個以上的職責。SRP 是 SOLID 五原則的第一個,本質是把「內聚性」綁到「誰會要求你改」上。
🧠 When to Use
TIP
訊號:改 A 功能卻莫名弄壞 B 功能;一個類別同時混了「商業規則」「畫面呈現」「持久化」;或某使用者只想用其中一小塊,卻被迫連帶依賴一整包不相關的東西。
- 一個類別的方法可以清楚分成「服務不同對象 / 不同變更來源」的幾群。
- 商業邏輯與 I/O(持久化、繪圖、格式化)混在同一個類別。
- 想讓「因不同理由而變」的程式碼各自獨立編譯、測試、部署。
⚖️ Structure & Variants
經典案例 — Rectangle 的雙重職責
// ❌ 同一個類別扛了兩個來自不同情境的職責
class Rectangle(val width: Double, val height: Double) {
fun area(): Double = width * height // 幾何計算 App 要用
fun draw() { /* 呼叫 GUI 繪圖 */ } // 圖形介面 App 要用
}
// GUI 需求一改 → Rectangle 改 → 純計算的 App 也被迫重編、重測、重部署
// ✅ 沿變更軸線切開:幾何歸幾何,繪圖歸繪圖
class Rectangle(val width: Double, val height: Double) {
fun area(): Double = width * height
}
class RectangleView(private val rect: Rectangle) {
fun draw() { /* 繪圖只依賴幾何資料,不反過來污染它 */ }
}- 兩個職責來自不同的使用情境,就有兩條獨立的變更軸線,該分開。
介面層先分離 — Modem 的『圍籬後的妥協』
// 一個 Modem 混了「連線管理」和「資料通訊」兩個職責
interface DataChannel { fun send(c: Char); fun recv(): Char } // 資料通訊
interface Connection { fun dial(number: String); fun hangup() } // 連線管理
// 實作可以暫時仍在同一個類別,但客戶端只依賴自己需要的那個介面
class ModemImpl : DataChannel, Connection {
override fun send(c: Char) { /* ... */ }
override fun recv(): Char = TODO()
override fun dial(number: String) { /* ... */ }
override fun hangup() { /* ... */ }
}- 即使實作內部還沒完全解耦,介面分離已足以保護客戶端不感知這種耦合——書中稱之為「圍籬後的妥協(a kludge behind a fence)」。
⚠️ Misuse & Anti-patterns
- 在沒有症狀時硬拆:變更軸線只有在「變更真的發生」時才有意義。沒有實際的變更壓力就預先切碎,只會引入不必要的複雜性(Needless Complexity)。
- 把「職責」理解成「方法數」:職責是「變更的理由」,不是「方法多寡」。方法很多但都服務同一個 actor,仍是內聚的。
- God Object:反過來的極端——一個類別包山包海。這是 SRP 最常見的違反形態(見 反模式辨識)。
🔑 Takeaways
- 職責 = 變更的理由;一個以上的理由要改它 = 承擔了一個以上的職責。
- SRP 與「內聚性」是一體兩面:內聚的類別,所有功能服務同一個變更來源。
- 難在拿捏時機:找出並分離職責是設計的藝術,但別在沒有變更症狀時預先過度切割。
No notes yet — jot your takeaways or Q&A here.