《Clean Architecture》Podcast 準備稿:SOLID 下篇與元件內聚——ISP、DIP、元件三原則
書名: 整潔的軟體架構 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 下+元件:依賴反向,與那個沒有正確答案的張力三角 (4/10) 涵蓋範圍: 第三部分第 10、11 章(ISP、DIP)+ 第四部分第 12、13 章(什麼是元件、元件內聚性)
背景速覽
上一集我們講了 SOLID 的前三個(SRP、OCP、LSP),主題是「隔離改變的衝擊半徑」。這集是 SOLID 的下半場——ISP 和 DIP——然後我們會跨過一道門檻,從「類別層次」正式進入「元件層次」。
這個跨越是全書的轉折點。前面五個 SOLID 原則談的是怎麼安排類別、方法、介面;從第四部分「元件原則」開始,Uncle Bob 把鏡頭拉遠,問一個更大的問題:我們該怎麼決定哪些類別打包在一起、變成一個可獨立部署的單位(.jar / .dll / Gem)?
有意思的是,你會發現元件層次的兩個內聚原則(CCP、CRP)其實就是 SRP 和 ISP 的放大版。Uncle Bob 的整套理論有一種「碎形」的美感——同樣的張力,在類別尺度和元件尺度上反覆出現。這集的高潮是那個「張力三角」:三個內聚原則彼此拉扯,而架構師的工作就是在三角形裡找一個會隨時間移動的落點。沒有正確答案——這正是它誠實的地方。
一句話重點
ISP 與 DIP 收尾了「依賴規則」的理論(別依賴你不用的、依賴抽象別依賴具體);而元件內聚的 REP/CCP/CRP 三原則彼此制衡成一個「張力三角」,逼架構師承認:打包邊界沒有標準答案,只有隨專案階段移動的動態平衡。
值得討論的重點
1. ISP——別依賴你不需要的東西(以及它對動態語言沒那麼重要)
ISP 的定義很直觀:「不應強迫客戶端依賴它不使用的方法。」 書裡的例子是一個胖類別 OPS 有 op1/op2/op3,三個 User 各只用一個方法。如果 User1 直接依賴整個 OPS,那當 User2 要求改 op2 時,OPS 原始碼變了,User1 明明沒用到也得被迫重新編譯部署;更糟的是,如果 op2 的改動讓 OPS 編譯壞掉,User1 也跟著掛。
解法是把介面切開:U1Ops / U2Ops / U3Ops,各自只含自己要的方法,OPS 去實作這三個。
這裡有個關鍵但常被略過的細節:ISP 主要是靜態型別語言(Java/C++)的問題。因為原始碼層級的依賴會強制重編譯。動態語言(Python/Ruby)因為依賴是執行期推斷的,這個問題輕得多。Uncle Bob 誠實地寫了這點——這也預告了一個更大的辯證:很多 SOLID 原則的「必要性」其實綁在特定語言/工具鏈的限制上。
2. DIP——依賴抽象、別依賴具體,以及那個「必要的髒污」
DIP 是全書最有深度的原則,也是「依賴規則」最直接的種子:「靈活的系統,其原始碼依賴只該指向抽象,不該指向具體。」 在 Java 裡就是:import 只該指向介面/抽象類別,不該指向具體實作。
為什麼?因為抽象穩定、具體易變。改一個介面通常得改所有實作(成本高,所以介面不常改);改一個實作通常不用動介面。架構師的策略就是:依賴穩定的東西、避開易變的東西。
但 DIP 有個無法迴避的矛盾:程式總得在某處 new 出具體物件。書裡的解法是兩層:
- 抽象工廠(Abstract Factory): 把「建立物件」這件事本身也藏到介面後面,讓控制流程和依賴方向反向——
Application依賴ServiceFactory(介面),但實際是ServiceFactoryImpl去new具體類別。 - Main 元件——必要的髒污: 我們無法 100% 消除對具體類別的依賴,總得有人啟動系統、建立第一個物件。Uncle Bob 的設計是把所有「骯髒」的具體依賴集中在
Main,讓系統其他部分保持純淨(只依賴抽象)。這個「把髒污集中到一個邊界」的思路,是整本書反覆出現的招式。
3. 元件是什麼——以及它為何是一部「與硬體搏鬥」的歷史
跨入第四部分,Uncle Bob 先給定義:「元件是部署的最小單位。」(.jar / Gem / .dll / 連結後的執行檔)。理想上元件要能獨立部署且獨立開發。
這章有一段很迷人的「元件簡史」,值得在節目裡講,因為它解釋了「為什麼現代外掛架構成為可能」:從史前時代的絕對記憶體位址(用函式庫得確保位址不衝突)、到可重定位二進位檔 + 連結器(但連結慢到要數小時)、再到摩爾定律讓硬體變快、最後連結載入器讓我們能在載入時甚至執行時瞬間完成連結。正因為連結變得又快又便宜,我們才能把系統拆成一堆 .jar/.dll、像外掛一樣隨時抽換更新而不必重編整個系統。外掛架構不是設計出來的理念,是硬體進步順出來的紅利。
4. 元件內聚的張力三角——這集真正的主菜
哪些類別該放進同一個元件?Uncle Bob 給三個原則,而它們彼此拉扯:
- REP(重用/發佈等價): 重用的粒度就是發佈的粒度。要讓人重用你的元件,就得做版本管理(v1.0/v1.1)、變更紀錄。同一元件內的類別應有相同的發佈策略。→ 傾向把元件做大。
- CCP(共同封閉): 把「會因相同理由、在相同時間改變」的類別放一起。這是元件層級的 SRP。理由是可維護性——改了 DB schema 就得改對應 Mapper,那它們就該綁一起,改一次只重部署一個元件。→ 傾向把元件做大。
- CRP(共同重用): 別強迫使用者依賴他不需要的東西。這是元件層級的 ISP。只有「幾乎總是一起被用」的類別才該放一起,否則 B 裡一個你沒用到的類別改了,你還是得重編。→ 傾向把元件做小。
於是 REP+CCP(包容,做大)和 CRP(排除,做小)互相對抗,形成「張力三角」:
- 只顧 REP+CCP(丟掉 CRP):太多不必要的發佈,元件太大被迫依賴沒用的東西。
- 只顧 CCP+CRP(丟掉 REP):難以重用,缺版本管理。
- 只顧 REP+CRP(丟掉 CCP):一個簡單需求變更得同時改好幾個元件。
結論是動態的: 專案初期重「可發展性」,偏 CCP+CRP(右側);專案成熟、別人開始用你的元件後,重「可重用性」,重心向 REP(左側)移動。好架構師不死守任一原則,而是根據專案當下狀態在三角形裡挪動落點。
注意事項
⚠️ DIP「依賴抽象」在不同世界的份量差很多——這是本集最重要的辯證。在有 DI 框架(Spring)、或在動態語言裡,「依賴反向」幾乎是免費的、自然的。但在缺乏這些工具的場景,為了 DIP 而「到處手動加介面 + 抽象工廠」很容易變成過度抽象。John Ousterhout 在《A Philosophy of Software Design》裡的**「淺模組(shallow module)」**批評正中要害:一個介面只有一個實作、介面和實作幾乎一樣厚,這種抽象沒有藏起任何複雜度,只是增加了一層要維護的樣板。Ousterhout 主張「深模組」——簡單介面藏住大量複雜度。節目裡務必誠實帶出:DIP 是工具不是教條,「每個類別都先抽一個介面」往往是 cargo cult 而非好設計。
⚠️ ISP 的價值高度綁定靜態型別 + 編譯依賴。Uncle Bob 自己都承認動態語言裡這問題輕很多。在 2025 年的視角看,這提醒我們:SOLID 不是放諸四海皆準的物理定律,它有強烈的「1990-2000 年代 Java/C++ 工程環境」的時代烙印。這不是否定它,而是要懂得分辨「哪些是永恆道理、哪些是特定工具鏈的權宜」。
⚠️ 「元件 = 部署單位」這個定義在微服務和 monorepo 時代有點微妙。在大型 monorepo 裡,部署單位和「程式碼組織單位」可能脫鉤;在微服務裡,一個服務可能就是一個部署單位但內部又切了很多模組。Uncle Bob 的 .jar/.dll 心智模型偏單體應用時代。內聚三原則的「精神」(內聚力、改變的擴散)依然成立,但「元件」的物理邊界要重新對應到你的實際部署形態。
⚠️ 張力三角的「沒有正確答案」是它的優點也是它的陷阱。優點:它誠實承認 trade-off。陷阱:它幾乎無法證偽——任何打包決定事後都能用「我在三角形裡選了個落點」來合理化。在節目裡可以點出:這類框架的真正用途是讓你在做決定前先想清楚自己在優化什麼(可發展性?還是可重用性?),而不是事後拿來自圓其說。
專家補充
💡 DIP 的「Main 集中髒污」其實是更大模式的一個實例,後面架構章節會把它升級成完整的「Main 元件」(第 26 章)。這個「把無可避免的不純淨集中到系統最外緣一個點」的思路,和函數式編程裡的「functional core, imperative shell」異曲同工。對熟 FP 的聽眾來說,這個類比能瞬間打通。
💡 CCP 是元件層 SRP、CRP 是元件層 ISP——這個「碎形對應」不是巧合,是 Uncle Bob 刻意的設計。值得在節目裡點明,因為它讓五個 SOLID 和元件三原則之間有了清晰的橋:SOLID 管類別內的改變擴散,元件原則管元件間的改變擴散,同一套「最小化耦合、隔離變更」的哲學在兩個尺度重演。
💡 抽象工廠 + DIP 的組合在現代框架裡幾乎被「依賴注入容器」吃掉了。Spring、Guice、Dagger 做的就是「在啟動時把具體實作注入到只認識介面的高層」——本質就是書裡的 Abstract Factory + Main。所以今天的工程師其實天天在用 DIP,只是框架幫你把 Main 那層髒污自動化了。知道這個對應,讀這章會有「啊原來我一直在做這件事」的恍然。
討論問題
🎙️ 你的程式碼裡有沒有那種「一個介面只有唯一一個實作」的抽象?它真的藏住了複雜度(深模組),還是只是為了 DIP 而加的樣板(淺模組)?
🎙️ 用張力三角看你現在的專案:你覺得你的團隊現在更需要「可發展性」(改得快)還是「可重用性」(版本穩定給別人用)?你的元件切分有反映這個階段嗎?
🎙️ 你的某個共用函式庫,有沒有那種「我只用了它 10% 的功能,卻被迫扛進它全部依賴」的痛?那就是 CRP / ISP 在現實中被違反的樣子——你會怎麼拆?
🎙️ 如果今天用的是 Spring 這類 DI 框架,DIP 對你來說還是一個「要刻意遵守的原則」,還是已經變成框架自動幫你做的事?這對「該不該手動加介面」有什麼啟發?
更大範圍關聯
- 接續上集(SOLID 上): 上集的 SRP/OCP/LSP + 這集的 ISP/DIP 共同完成了「依賴規則」的理論底座。特別是 DIP——「依賴指向抽象」——就是下集和全書後半那張「Clean Architecture 同心圓」的核心引擎:讓所有依賴箭頭跨越邊界時都指向更高層、更抽象的內圈。
- 碎形對應: 這集最漂亮的串接是 CCP=元件版 SRP、CRP=元件版 ISP。記住這個,你就抓到了 Uncle Bob 整套方法論的骨架:同一組張力,在類別和元件兩個尺度上重演。
- 與 Ousterhout《A Philosophy of Software Design》: 這是 DIP 最強的反方對話。Uncle Bob 說「依賴抽象」,Ousterhout 提醒「別造淺模組」。兩人不矛盾——合起來才完整:要抽象,但要抽象到能藏住複雜度的程度。
- 與《Refactoring》: 內聚三原則描述「好的元件邊界長怎樣」,但邊界是演進出來的(書裡明說元件結構不是第一天就設計好、而是隨系統發展)。Fowler 的 Move Class、Extract Module 就是在三角形裡移動落點的具體手法。
- 與下集(元件耦合與架構): 這集講「元件內部怎麼內聚」,下集講「元件之間怎麼耦合」(ADP/SDP/SAP)以及「什麼是架構」。內聚決定打包邊界,耦合決定依賴箭頭——兩集合起來才是完整的元件理論。
錄製建議
目標時長約 20 分鐘,單人主講。建議節奏:
- 開場 2 分鐘: 點明這集的轉折意義——「我們要從類別跨進元件了」。用「碎形」這個意象開場:你即將看到同樣的張力被放大一個尺度。
- SOLID 收尾 7 分鐘:
- ISP(3 分鐘)——
OPS三方法的例子講「被迫重編」,然後立刻誠實補上「但這主要是靜態語言的問題」,把辯證提早埋進來。 - DIP(4 分鐘)——花重一點,因為它是全書引擎。講「抽象穩定、具體易變」,然後「Main 必要的髒污」這個畫面要講清楚,它後面還會回來。
- ISP(3 分鐘)——
- 元件 9 分鐘:
- 元件定義 + 簡史(3 分鐘)——「外掛架構是硬體順出來的紅利」這句話當鉤子,連結載入器的故事很適合口語講。
- 張力三角(6 分鐘)——這是本集主菜,給最多時間。三個原則各一句話講清「傾向做大還是做小」,然後重點放在「三種偏廢各會出什麼事」和「落點隨專案階段移動」。務必強調「沒有正確答案」是誠實而非缺陷。
- 辯證貫穿: 本集的辯證不集中在尾段,而是邊講邊埋——ISP 講完帶語言侷限、DIP 講完帶 Ousterhout 淺模組。最後用 30 秒把「DI 框架已經自動化了 DIP」這個現代視角收束,給工程師聽眾「原來我天天在做」的共鳴。
- 收尾 1 分鐘: 拋一個討論問題(建議用「你有沒有一個介面只有一個實作」那題,最戳中現代工程師),預告下集——元件之間的耦合 + 第一次正面回答「什麼是架構」。
策略提醒:這集是五集裡概念最密的一集(七個縮寫:ISP/DIP/REP/CCP/CRP + 元件 + Main)。對抗資訊過載的方法是死守一條主線:「同一種張力,在越來越大的尺度上重演。」每講一個新縮寫,都把它掛回這條主線。張力三角務必用「做大 vs 做小」這個身體感很強的對立去講,不要陷進公式。公式(I 指標那些)留到下集再上。