本章介紹 依賴反轉原則(DIP)——物件導向設計與程序式設計之間最根本的差異所在。DIP 是建構良好分層架構的基礎,也是框架設計的核心機制。

原則定義#

DIP 包含兩個部分:

A. 高階模組不應該依賴低階模組。兩者都應該依賴抽象。 (High-level modules should not depend on low-level modules. Both should depend on abstractions.)

B. 抽象不應該依賴細節。細節應該依賴抽象。 (Abstractions should not depend upon details. Details should depend upon abstractions.)

違反症狀#

  • 高階策略模組直接依賴低階實作模組——當低階模組變更時,高階模組被迫一起修改
  • 高階模組無法被重用於其他情境,因為它被綁死在特定的低階實作上
  • 系統的**不可移動性(Immobility)**增加

傳統的分層架構問題#

Figure 11.1: Naive layering scheme

傳統的分層架構中,高階的 Policy Layer 依賴 Mechanism Layer,Mechanism Layer 又依賴 Utility Layer。這種遞移依賴(transitive dependency)意味著 Policy Layer 間接依賴了所有底層的細節。

問題在於:高階模組包含了應用程式的重要商業決策與策略,是我們最希望能夠重用的部分。但當它們依賴低階模組時,低階模組的任何變更都會波及高階模組,而且高階模組也無法獨立於低階模組被部署到其他情境。

反轉的分層架構#

正確的做法是讓每一層都依賴於抽象介面:Policy Layer 定義並依賴 PolicyServiceInterface,Mechanism Layer 實作這個介面並同時定義 MechanismServiceInterface,Utility Layer 實作 Mechanism 的介面。

每一層的介面都由使用它的那一層所擁有(owned by),而非由實作它的那一層所擁有——這就是所有權反轉(Ownership Inversion)

重點: DIP 的「反轉」不僅是依賴方向的反轉,更是所有權的反轉。高階模組定義介面(「你要為我服務就得遵守這個合約」),低階模組實作介面。這就是 Hollywood 原則——「Don’t call us; we’ll call you」。

依賴於抽象的經驗法則#

Martin 提出一個簡化的經驗法則:

  • 不要讓任何變數持有具體類別的參考
  • 不要讓任何類別繼承自具體類別
  • 不要覆寫基底類別中已實作的方法

補充: 這些規則在實務中經常被違反——例如 string 是具體類別,我們不可能避免依賴它。這個經驗法則主要適用於容易變化的具體類別。如果一個具體類別不太可能改變(如 string),依賴它並無大礙。

案例一:Button 與 Lamp#

天真的設計#

Figure 11.3: Naive model of a Button and a Lamp

一個 Button 物件偵測使用者的開關動作,並控制一個 Lamp 物件。天真的設計中,Button 直接依賴 Lamp

public class Button
{
    private Lamp lamp;

    public void Poll()
    {
        if (/* 某條件 */)
            lamp.TurnOn();
    }
}

這意味著 ButtonLamp 緊密耦合——Button 無法控制馬達、風扇或任何其他裝置。

套用 DIP#

Figure 11.4: Dependency inversion applied to Lamp

引入抽象介面 SwitchableDevice(一開始可能叫 ButtonServer,但更通用的名稱更好):

public interface ISwitchableDevice
{
    void TurnOn();
    void TurnOff();
}

public class Button
{
    private ISwitchableDevice device;

    public Button(ISwitchableDevice device)
    {
        this.device = device;
    }

    public void Poll()
    {
        if (/* 某條件 */)
            device.TurnOn();
    }
}

現在 Button 依賴抽象的 ISwitchableDevice,而 Lamp 實作這個介面。Button 可以控制任何實作 ISwitchableDevice 的裝置——完全不需要修改 Button 本身。

技巧: 介面的命名應該反映使用它的高階模組的語境,而非實作它的低階模組。ISwitchableDeviceILamp 好,因為 Button 不只是為了控制燈泡而存在。

案例二:Furnace(暖爐)與 Thermostat(恆溫器)#

天真的設計#

const byte THERMOSTAT = 0x86;
const byte FURNACE = 0x87;
const byte ENGAGE = 1;
const byte DISENGAGE = 0;

void Regulate(double minTemp, double maxTemp)
{
    for (;;)
    {
        while (In(THERMOSTAT) > minTemp)
            Wait(1);
        Out(FURNACE, ENGAGE);

        while (In(THERMOSTAT) < maxTemp)
            Wait(1);
        Out(FURNACE, DISENGAGE);
    }
}

這段「調節」演算法(高階策略)直接綁死在硬體 I/O 位址上(低階細節)——完全不可重用。

套用 DIP#

Figure 11.5: Generic regulator

定義抽象介面 ThermometerHeater,讓 Regulate 函式依賴這些介面:

public interface IThermometer
{
    double Read();
}

public interface IHeater
{
    void Engage();
    void Disengage();
}

void Regulate(IThermometer t, IHeater h,
              double minTemp, double maxTemp)
{
    for (;;)
    {
        while (t.Read() > minTemp)
            Wait(1);
        h.Engage();

        while (t.Read() < maxTemp)
            Wait(1);
        h.Disengage();
    }
}

現在調節演算法完全與硬體無關。它可以控制任何溫度計與加熱器的組合——無論是暖爐、熱水器還是核反應爐(但願不需要)。

本章小結#

依賴反轉原則是良好物件導向設計的標誌(the hallmark of good object-oriented design)。對程式語言而言,它意味著高階策略模組依賴抽象介面,而低階實作模組實作這些介面。對框架設計而言,它確保框架不被綁定在特定的實作上,使用者可以透過實作介面來擴展框架的行為。不論應用場景為何,DIP 的核心精神始終一致:依賴抽象,不依賴具體