《Clean Architecture》Podcast 準備稿:SOLID 上篇——SRP、OCP、LSP
書名: 整潔的軟體架構 Clean Architecture(A Craftsman’s Guide to Software Structure and Design) 作者: Robert C. Martin(Uncle Bob) 系列: Reading Uncle Bob(恩普拉氏) 公開標題建議: Reading Uncle Bob|Clean Architecture.SOLID 上:你以為的「單一職責」可能整個搞錯了 (3/10) 涵蓋範圍: 第三部分「設計原則」第 7、8、9 章——SRP 單一職責、OCP 開放封閉、LSP Liskov 替換
背景速覽
這是「Reading Uncle Bob」系列第二本書的第三集。前兩集我們從《Clean Code》一路讀到《Clean Architecture》的開場,建立了「架構是為了支撐生命週期、降低成本」的大方向。從這集開始,我們進入全書的第一個硬核區塊:SOLID 五大設計原則。
SOLID 是 Uncle Bob 的招牌——這五個字母幾乎是他個人品牌的一部分。但有個常被忽略的事實:這五個原則裡,只有 SRP 是他自己命名整理的,其他四個分別來自 Bertrand Meyer(OCP,1988)、Barbara Liskov(LSP,1988)等人。Uncle Bob 的貢獻是把它們收斂成一套好記的縮寫,並且——這點很關鍵——把它們從「類別層次」的物件導向技巧,重新詮釋為「架構層次」的元件劃分原則。
這集講前三個:SRP、OCP、LSP。它們有一條暗線串起來:都是在問「當需求改變時,改動會擴散到哪裡?」——SRP 管「誰來要求改」,OCP 管「改的時候要不要動到舊碼」,LSP 管「替換實作時會不會破壞契約」。
一句話重點
SOLID 前三原則的共同主題是「隔離改變的衝擊半徑」:SRP 讓不同 actor 的改變互不波及、OCP 讓擴充不需修改既有程式碼、LSP 讓子型別能安全替換而不污染上層邏輯。
值得討論的重點
1. SRP 不是「一個函式只做一件事」——它是「對一個 actor 負責」
這是全書最容易被誤解的點,書裡開門見山就說了。多數工程師把 SRP 理解成「每個模組只做一件事(Do one thing)」,但 Uncle Bob 明說那是重構(Refactoring)層次的概念,不是架構層次的 SRP。
架構層次的 SRP 真正定義是:「一個模組應只有一個理由可以被改變。」 而「改變的理由」精確翻譯就是——「模組應只對唯一一個 actor(角色)負責。」
書裡那個經典的 Employee 類別把這講得很清楚:一個類別同時有 calculatePay()(服務 CFO/財務)、reportHours()(服務 COO/人資)、save()(服務 CTO/技術)。三個方法服務三個不同部門,這就是違反 SRP。
2. 違反 SRP 的真正代價:意外重複與合併衝突
要在節目裡把「為什麼這很糟」講具體,書裡給了兩個症狀:
- 意外重複(Accidental Duplication)——最致命的一個。假設
calculatePay和reportHours共用一個私有方法regularHours()算工時。CFO 要求改工時算法,工程師改了regularHours(),滿足了財務,卻意外改壞了人資的報表,而人資完全不知情。書裡的設定是:帶 bug 上線,數百萬美元損失。 - 合併衝突(Merge)——DBA 為了資料庫改
save(),財務人員為了新薪資政策改calculatePay(),兩個人為了完全不同的理由動同一個檔案,於是版控衝突、合併出錯。
解法是分離:把資料結構 EmployeeData(純資料)跟三個獨立類別 PayCalculator / HourReporter / EmployeeSaver 拆開,它們互不認識。如果嫌呼叫端要實例化三個太麻煩,再套一層 Facade(外觀模式) 統一入口。
3. OCP 的真正機制是「依賴方向」,不是「繼承與介面」
OCP 的定義——「對擴充開放、對修改封閉」——大家都會背。但很多人以為 OCP 就是「多用介面、多用繼承」。書裡點破:那只是類別層次的理解。架構層次的 OCP,核心是控制原始碼依賴的方向。
書裡用財務報表的例子:系統原本只出 Web 報表,現在要加 Print 報表。理想狀況是——只新增一個 Print Presenter,完全不動核心計算邏輯(Interactor)。怎麼做到?把系統分成高層(核心業務規則,最不該變)和低層(DB、Web Controller、Presenter,常變),然後強制所有依賴箭頭都指向高層。這樣 DB 或 Web 改變時,核心 Interactor 一行都不用改(對修改封閉);要加新展示方式時只要擴充新 Presenter(對擴充開放)。這也是全書「依賴規則」的雛形,後面元件章節會再回來。
4. LSP 的正方形/長方形反例,以及它早就超出繼承範疇
LSP 的定義是「子型別必須能替換父型別而不改變程式的正確行為」。書裡那個招牌的鴨子比喻很傳神:「看起來像鴨子、叫起來像鴨子,但要裝電池才能動——它就違反 LSP。」
最經典的反例是正方形 vs 長方形:數學上正方形是長方形,但行為上不是。長方形的契約是「寬高獨立,改寬不影響高」;正方形為了維持自身特性,改寬必須同時改高。於是當上層拿著 Rectangle 參考卻被塞進一個 Square,設定寬度時高度也跟著變,面積算錯。為了補救,上層被迫加 if (type == Square)——這就是架構被污染的起點。
更值得在節目裡強調的是:書裡把 LSP 拉到架構層級。那個計程車調度的例子——一家叫 Acme 的車行不遵守 URI 約定,把 destination 縮成 dest,結果核心程式碼被迫加 if (uri.startsWith("acme.com"))。具體公司名入侵核心邏輯,每多一家違規業者就改一次。LSP 違反的代價不只是程式碼變醜,而是整個系統被迫增加一套查表/格式化機制來對抗不一致。所以 LSP 適用的不只是 Java 繼承,還有 REST 介面、duck typing——任何「使用者依賴定義良好的介面、且期待實作可互換」的場景。
注意事項
⚠️ SOLID 是 Uncle Bob 的招牌,但近年有一場很值得知道的辯論:SOLID 是否被高估或被誤用?Dan North(BDD 的提出者)寫過一篇有名的〈CUPID — for joyful coding〉,前身常被引述為「SOLID is not solid」的論點。他的批評是 SOLID 太抽象、太容易被當教條背誦,並提出替代的 CUPID 五個「特質」(Composable / Unix philosophy / Predictable / Idiomatic / Domain-based)。重點不是「SOLID 錯了」,而是——把它當成思考工具而非打勾清單。節目裡誠實帶出這個張力,比單純複誦五原則更有價值。
⚠️ SRP 的「一個改變的理由」出了名地難定義。連 Uncle Bob 自己都在後續文章(〈The Single Responsibility Principle〉部落格)裡承認原始措辭容易誤導,才把它澄清成「對一個 actor 負責」。實務上「誰算同一個 actor」本身就是判斷題——財務和會計算不算同一個 actor?這種模糊性意味著 SRP 不能機械式套用,要靠對業務的理解。可以在節目裡誠實說:這條原則的價值在於「逼你問對問題」,而不是給你標準答案。
⚠️ 正方形/長方形反例雖然經典,但它其實是個有點刻意的教學案例。實務中沒人會真的去 class Square extends Rectangle。它的真正價值是揭示繼承的契約陷阱,而不是字面上的幾何問題。在現代程式設計裡,「組合優於繼承」(composition over inheritance)的共識,某種程度上已經從根本上繞開了很多 LSP 違反的場景。可以帶出:LSP 教我們的更深一層教訓是「型別系統不保證行為相容」。
⚠️ OCP 的「對修改封閉」常被過度詮釋成「永遠不准改舊碼」,進而導致為了預測未來而到處預埋抽象——這是過度設計的溫床。Uncle Bob 自己的態度其實是務實的:OCP 是讓你「面對已知會變的軸向」做好隔離,不是要你預測所有未來。這跟後面 Ousterhout 的「淺模組」批評會呼應,先埋個伏筆。
專家補充
💡 SRP 與 SOLID 裡其他原則的歷史關係:Uncle Bob 在《Agile Software Development》(2002)就整理過這套原則,SOLID 這個縮寫的排序是後來為了好記才定的(由 Michael Feathers 建議)。所以五個字母的順序沒有邏輯遞進關係,純粹是記憶術。知道這點能避免聽眾誤以為「要先做 S 再做 O」。
💡 LSP 的學術根源很硬。Barbara Liskov 1987 年的演講、1994 年與 Jeannette Wing 合寫的論文,正式定義了「行為子型別(behavioral subtyping)」,其中包含前置條件不可加強、後置條件不可削弱、不變量必須保留等規則。Uncle Bob 書裡的版本是高度簡化的工程版。對工程師聽眾來說,知道「前置條件不可加強、後置條件不可削弱」這組更精確的判準很實用——這才是判斷「能不能安全替換」的操作型標準。
💡 OCP 的「依賴反向」其實預告了 DIP(下集會講)。OCP 描述「我們想要什麼結果」(高層不受低層影響),DIP 描述「靠什麼機制達成」(依賴抽象)。書的編排刻意把 OCP 放前面、DIP 放最後,讓你先看到目標再看到手段。節目裡可以點出這個敘事結構。
討論問題
🎙️ 回想你最近一次「改 A 結果壞了 B」的事故——如果用 SRP 的「actor」視角回頭看,當初那兩段邏輯是不是其實服務著不同的 actor、本來就該分開?
🎙️ 你的團隊有沒有那種「為了支援某個不守規矩的外部系統而加的 if 特例」?那段 if 現在長成什麼樣子了?這就是 LSP 在架構層級被違反的活標本。
🎙️ 如果要在「嚴守 OCP、到處預埋介面」和「先寫死、等真的要變再重構」之間選,你現在的專案階段應該偏哪一邊?為什麼?
🎙️ 聽完 Dan North 對 SOLID 的質疑,你覺得 SOLID 對你來說更像是「設計指南」還是「事後用來說服別人的話術」?
更大範圍關聯
- 接續《Clean Code》: SRP 在《Clean Code》是函式/類別層級的「函式只做一件事」,在《Clean Architecture》被升維成「對一個 actor 負責」。同一個名字、兩種尺度——這正是兩本書的分工:一本講微觀工藝,一本講宏觀結構。
- SOLID 作為一個整體: 這集的 SRP/OCP/LSP 與下集的 ISP/DIP 共構成「依賴規則」的理論基礎,而依賴規則正是全書後半「Clean Architecture 同心圓圖」的核心。前三原則是「磚」,後面架構章節是「牆」。
- 與《Refactoring》(Martin Fowler): SRP 的「意外重複」「合併衝突」本質上是 code smell,Fowler 的重構手法(Extract Class、Move Method)正是消除這些 smell 的具體操作。Uncle Bob 給原則,Fowler 給手法。
- 與 Ousterhout《A Philosophy of Software Design》: Ousterhout 的「深模組 vs 淺模組」會在下集對 DIP 形成最強的反方論點,但這集的 OCP「不要為了封閉而過度抽象」已經先和他握手了。
錄製建議
目標時長約 20 分鐘,單人主講。建議節奏:
- 開場 2 分鐘: 先講 SOLID 的「品牌地位」和那個冷知識——五個原則只有一個是 Uncle Bob 自己取的、縮寫順序純為好記。這個反差能立刻抓住工程師聽眾。
- 主體 14 分鐘,三段式:
- SRP(5 分鐘)——花最多時間,因為「actor」這個重新定義是本集最有記憶點的 takeaway。用
Employee三部門的故事講「意外重複」,具體到「數百萬損失」那個畫面。 - OCP(4 分鐘)——重點打在「機制是依賴方向、不是繼承」,用財務報表 Web→Print 的例子。
- LSP(5 分鐘)——正方形/長方形快速帶過(它是反例不是重點),把時間留給計程車 Acme 的架構污染故事,那個
if startsWith("acme")的畫面最有共鳴。
- SRP(5 分鐘)——花最多時間,因為「actor」這個重新定義是本集最有記憶點的 takeaway。用
- 辯證 3 分鐘: 集中火力在「注意事項」的前兩點——Dan North 的 CUPID、SRP 的定義困難。這是本集「吸收+辯證」招牌調性的高潮,語氣要誠實:「我很喜歡 SOLID,但我必須告訴你它被誤用得有多嚴重。」
- 收尾 1 分鐘: 拋一個討論問題,預告下集 ISP + DIP + 元件,點明「下集會看到這些原則怎麼從『磚』砌成『牆』」。
策略提醒:這集概念密度高,務必用「故事先行、定義後到」的方式——先講 Employee 出事了,再給 SRP 定義;先講 Acme 污染了系統,再給 LSP 定義。工程師聽眾對「血淋淋的事故」遠比對「乾巴巴的定義」買單。