本章介紹 依賴反轉原則(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();
}
}這意味著 Button 與 Lamp 緊密耦合——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 本身。
技巧: 介面的命名應該反映使用它的高階模組的語境,而非實作它的低階模組。
ISwitchableDevice比ILamp好,因為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
定義抽象介面 Thermometer 與 Heater,讓 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 的核心精神始終一致:依賴抽象,不依賴具體。