當兩個或多個抽象直接或間接相互依賴時,就出現 Cyclically-Dependent Modularization。

核心定義#

這個壞味道違反 Acyclic Dependencies Principle (ADP)與 Ordering Principle。

帶來的代價:

  • 處於相依環中的抽象必須一起被理解、變更、使用、測試、重用
  • 改 A 影響 B,B 又會反過來影響 A——連鎖效應從同一處出發又繞回來
  • 大型、間接的環在複雜系統中極難察覺,是隱蔽 bug 的溫床。

名詞釐清#

  • 相依圖(dependency diagram):以有向邊表達抽象之間的相依關係。
  • 環(cycle):從一個抽象出發、沿邊行走、回到同一抽象的路徑。
  • 糾纏(tangle):多個環交織,產生視覺與語意上的「亂麻」。

真實案例:書中展示 java.util 套件中六個抽象之間糾結的環。

Figure 5.10: java.util 套件中六個抽象之間的循環

與 Cyclic Hierarchy 的差別#

繼承本身就是子型別對父型別的相依;若父型別也反過來依賴子型別,會產生特殊形式的循環,稱為 Cyclic Hierarchy(6.10 節)。本壞味道指的是非繼承關係的循環。

常見成因#

  • 責任放錯位置:成員擺到不該擺的類別後,彼此相互引用形成環。
  • 隨手傳 this:方法呼叫時直接把自己整個物件傳給對方,雙向暴露
  • 回呼(callback)實作不當:未使用 Observer 等樣式來解開對稱依賴。
  • 間接相依難以視覺化:在大型系統中肉眼追蹤不到環。

範例#

範例 1:SecureDocument ↔ DESEncryption#

SecureDocument encryptedDocument = desEncryption.encrypt(this);
// DESEncryption 中
SecureDocument encrypt(SecureDocument docToEncrypt);

SecureDocumentthis 傳給 DESEncryptionDESEncryption 反過來知道 SecureDocument 的所有東西——雙向相依

Figure 5.11: SecureDocument 與 DESEncryption 之間的循環相依

範例 2:Image ↔ Encryption#

醫療影像加密:SecurityManager 透過 Image 取得加密後內容,但 Image.encrypt() 內部又呼叫 Encryption,並把 this 傳過去——ImageEncryption 互依。

Figure 5.12: Image 與 Encryption 之間的循環相依

範例 3:Order ↔ OrderImpl#

Order.getAmount() 呼叫 OrderImpl.computeAmount()computeAmount() 又從 Order 取資料——彼此緊耦合。

Figure 5.13: Order 與 OrderImpl 之間的循環相依

範例 4:Order ↔ TaxCalculator#

Order.getAmount() 呼叫 TaxCalculator.computeAmount();後者讀 Order 的全部 items 並結合 calculateTax()——同樣雙向。

Figure 5.14: Order 與 TaxCalculator 之間的循環相依

建議重構#

核心策略:打破環。書中提供四種選項:

  • Option 1:為環中的某個抽象引入介面(並讓另一方依賴介面而非具體類別)。
  • Option 2:若某個方向的相依其實多餘,直接刪除。可借助 move methodmove field 把製造相依的程式碼搬到正確類別。
  • Option 3:把製造相依的程式碼搬到第三個抽象裡。
  • Option 4:若兩抽象其實在語意上是同一物件,合併為一

Figure 5.15: 類別 A 與 B 之間的循環相依

Figure 5.16: 透過引入介面打破循環相依(Option 1)

Figure 5.17: 透過移除相依打破循環(Option 2)

Figure 5.18: 透過引入另一個抽象打破循環(Option 3)

Figure 5.19: 透過合併抽象打破循環(Option 4)

flowchart LR
    subgraph Original
        A1[A] <-->|cyclic| B1[B]
    end
    subgraph "Option 1"
        A2[A] --> I[Interface] <-- B2[B]
    end
    subgraph "Option 2"
        A3[A] --> B3[B]
    end
    subgraph "Option 3"
        A4[A] --> M[Mediator] <-- B4[B]
    end
    subgraph "Option 4"
        AB[A∪B]
    end

對應到範例#

  • 範例 1(Option 1):抽出 IEncryption 介面,SecureDocument 依賴介面而非 DESEncryption。新加 AESEncryption 不再影響 SecureDocument

Figure 5.20: SecureDocument 與 DESEncryption 的建議重構(範例 1)

  • 範例 2(Option 2/3):把「對 Image 加密」這個責任移到 SecurityManager,由它直接呼叫 Encryption.encrypt(image)Image 不再依賴 Encryption

Figure 5.21: Image 與 Encryption 的建議重構(範例 2)

  • 範例 3(Option 4)OrderImpl 只有一個方法且語意上就是 Order,合併之。

Figure 5.22: Order 與 OrderImpl 的建議重構(範例 3)

  • 範例 4(Option 2):把 computeAmount() moveOrderTaxCalculator 不再依賴 Order

Figure 5.23: Order 與 TaxCalculator 的建議重構(範例 4)

軼事:缺乏架構師的新創公司#

一家新創公司原由資深架構師設計分層架構,但他離職後,由新人團隊在交期壓力下繼續開發。Business logic 跑進 Buttons/Panels;GraphUtilities 直接引用 GUI 類別。後來相依分析工具顯示跨層的相依環不計其數——任何改動都可能弄壞其他功能。

教訓:

  • 必須有專責架構師持續守護整體結構
  • 技術債若不定期透過重構償還,最終會把專案卡死

對品質屬性的衝擊#

  • 理解性:環中的抽象必須一起理解
  • 可變更性與可擴充性:改動會出現連鎖效應。
  • 可重用性:環中的抽象只能整組重用。
  • 可測試性:難以獨立測試。
  • 可靠性:相依鏈中的型別變動可能在他處引發執行期錯誤(如 double → float 改型,他處才出現浮點誤差)。

別名#

  • Dependency cyclesCyclic dependenciesCycles:類別有循環引用。
  • Bidirectional relation:兩個類別的方法雙向呼叫。
  • Cyclic class relationships:類別之間有共依賴等不當關係。

實務考量:單元環#

長度為 2 的環稱為 unit cycle,常出現在概念上密切相關的一對抽象之間。例如 java.util.regex.MatcherPattern:兩者幾乎總是一起使用。 Unit cycles 因為小、易管理,通常可以接受。真正可怕的是大型循環與糾纏——這些是真實系統難維護的根源。

Figure 5.24: Matcher 與 Pattern 之間的循環相依