Robert C. Martin 為這個原則提出兩條互相呼應的規則:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions(高階模組不應依賴低階模組,兩者都應依賴抽象)。
- Abstractions should not depend on details. Details should depend on abstractions(抽象不應依賴細節,細節應依賴抽象)。
這兩條規則合在一起,描述的是系統中依賴關係的方向性應該如何安排,要理解它們需要先理解「高階模組」和「低階模組」的差別。
高階模組與低階模組
- 高階模組(High-level modules):包含應用程式核心業務邏輯的模組,它們描述的是「這個系統要做什麼」:訂單需要被驗證、付款需要被處理、報表需要被產生;這些邏輯代表了系統存在的根本原因,是具有業務價值的部分。
- 低階模組(Low-level modules):提供具體技術實作的模組,它們描述的是「這件事具體怎麼做」:資料存到 SQL Server、Email 用 SMTP 發送、圖片存到 S3;這些模組是高階模組的工具,本身通常不含業務價值,可以被替換。
在傳統的依賴方向中,高階模組直接使用(依賴)低階模組:
OrderService(高階)→ SqlServerRepository(低階)
這意味著業務邏輯層與具體的資料庫技術「綁死」在一起;若今天想把 SQL Server 換成 PostgreSQL,就必須修改 OrderService 這個本來應該只關心業務規則的類別,DIP 要「反轉」的正是這個依賴方向。
與 OOP 的關係:多型(Polymorphism)與抽象(Abstraction)
DIP 的實現依賴抽象與多型,具體的做法是在高階模組與低階模組之間插入一層抽象(介面),讓兩者都依賴這個介面,而非彼此直接依賴。
傳統方向(高耦合):
OrderService ─────────────────→ SqlServerRepository
(高階) (低階,具體實作)
DIP 方向(低耦合):
OrderService ──→ IOrderRepository ←── SqlServerRepository
(高階) (抽象介面) (低階,具體實作)
在 DIP 的架構下,依賴箭頭的方向發生了「反轉」:
OrderService(高階)依賴IOrderRepository(抽象)。SqlServerRepository(低階)也依賴IOrderRepository(抽象),透過「實作」的方式。
這個「反轉」帶來了一個關鍵的好處:高階模組不再需要知道低階模組的存在;若要把 SQL Server 換成 PostgreSQL,只需要新增一個 PostgresSqlRepository 類別,OrderService 不需要改動。
這也是 DIP 命名的由來:原本是高階依賴低階,現在反轉成低階依賴(實作)高階定義的抽象。
程式碼範例
違反 DIP:直接依賴具體實作
1// 低階模組:具體的資料庫存取實作
2public class SqlOrderRepository
3{
4 private readonly string _connectionString;
5
6 public SqlOrderRepository(string connectionString)
7 => _connectionString = connectionString;
8
9 public void Save(Order order)
10 {
11 // 具體的 SQL 操作
12 using var conn = new SqlConnection(_connectionString);
13 // INSERT INTO Orders...
14 Console.WriteLine($"[SQL] 儲存訂單 #{order.Id}");
15 }
16
17 public Order? FindById(int id)
18 {
19 using var conn = new SqlConnection(_connectionString);
20 // SELECT * FROM Orders WHERE Id = @id
21 Console.WriteLine($"[SQL] 查詢訂單 #{id}");
22 return new Order { Id = id };
23 }
24}
25
26// 高階模組:直接依賴具體的低階類別
27public class OrderService
28{
29 // 問題一:直接 new 出低階類別,高階與低階緊密耦合
30 private readonly SqlOrderRepository _repo
31 = new SqlOrderRepository("Server=...;Database=Orders;");
32
33 // 問題二:若想換成 PostgreSQL,必須修改這個類別
34 // 問題三:無法在沒有 SQL Server 的情況下單獨測試業務邏輯
35 public void PlaceOrder(Order order)
36 {
37 if (order.Total <= 0)
38 throw new ArgumentException("訂單金額必須大於零");
39 if (order.Items.Count == 0)
40 throw new ArgumentException("訂單至少需要一個品項");
41
42 order.Status = OrderStatus.Pending;
43 order.CreatedAt = DateTime.UtcNow;
44 _repo.Save(order);
45
46 Console.WriteLine($"訂單 #{order.Id} 建立成功");
47 }
48}
這個設計的問題是具體且真實的:
- 若公司決定從 SQL Server 遷移到 PostgreSQL,
OrderService必須被修改,即使業務邏輯一點都沒有改變。 - 若想對
PlaceOrder中的業務驗證邏輯撰寫單元測試,測試必須連接真實的資料庫,讓測試變得脆弱且緩慢。 OrderService無法在不同環境(生產環境用 SQL、測試環境用記憶體)中彈性切換儲存機制。
遵守 DIP:依賴抽象,透過建構子注入
1// 第一步:定義抽象介面
2// 注意:這個介面由「業務邏輯層」定義,描述的是「業務邏輯需要什麼能力」
3// 而不是「資料庫能提供什麼功能」——這是「反轉」的體現
4public interface IOrderRepository
5{
6 void Save(Order order);
7 Order? FindById(int id);
8 IEnumerable<Order> FindByCustomer(int customerId);
9}
10
11// 第二步:讓低階模組實作抽象介面
12// SQL Server 版本
13public class SqlOrderRepository : IOrderRepository
14{
15 private readonly string _connectionString;
16
17 public SqlOrderRepository(string connectionString)
18 => _connectionString = connectionString;
19
20 public void Save(Order order)
21 {
22 // 所有 SQL Server 相關的實作細節都在這裡
23 Console.WriteLine($"[SQL Server] 儲存訂單 #{order.Id}");
24 }
25
26 public Order? FindById(int id)
27 {
28 Console.WriteLine($"[SQL Server] 查詢訂單 #{id}");
29 return new Order { Id = id };
30 }
31
32 public IEnumerable<Order> FindByCustomer(int customerId)
33 {
34 Console.WriteLine($"[SQL Server] 查詢客戶 #{customerId} 的所有訂單");
35 return Enumerable.Empty<Order>();
36 }
37}
38
39// 無縫替換:PostgreSQL 版本,不影響 OrderService
40public class PostgreSqlRepository : IOrderRepository
41{
42 private readonly IMongoCollection<Order> _collection;
43
44 public MongoOrderRepository(IMongoDatabase db)
45 => _collection = db.GetCollection<Order>("orders");
46
47 public void Save(Order order)
48 {
49 // MongoDB 的實作細節
50 Console.WriteLine($"[MongoDB] 儲存訂單 #{order.Id}");
51 }
52
53 public Order? FindById(int id)
54 {
55 Console.WriteLine($"[MongoDB] 查詢訂單 #{id}");
56 return null;
57 }
58
59 public IEnumerable<Order> FindByCustomer(int customerId)
60 {
61 Console.WriteLine($"[MongoDB] 查詢客戶 #{customerId} 的訂單");
62 return Enumerable.Empty<Order>();
63 }
64}
65
66// 單元測試專用的記憶體版本
67// 不需要任何資料庫,測試跑得飛快,且完全可控
68public class InMemoryOrderRepository : IOrderRepository
69{
70 private readonly Dictionary<int, Order> _store = new();
71 private int _nextId = 1;
72
73 public void Save(Order order)
74 {
75 if (order.Id == 0) order.Id = _nextId++;
76 _store[order.Id] = order;
77 }
78
79 public Order? FindById(int id)
80 => _store.TryGetValue(id, out var order) ? order : null;
81
82 public IEnumerable<Order> FindByCustomer(int customerId)
83 => _store.Values.Where(o => o.CustomerId == customerId);
84}
85
86// 第三步:高階模組依賴抽象,透過建構子注入取得實作
87// OrderService 完全不知道、也不在乎 Repository 的具體實作是什麼
88public class OrderService
89{
90 private readonly IOrderRepository _repo;
91 private readonly ILogger _logger;
92
93 // 建構子注入(Constructor Injection):
94 // 所有依賴都透過建構子明確聲明,讓類別的依賴關係一目了然
95 public OrderService(IOrderRepository repo, ILogger logger)
96 {
97 _repo = repo;
98 _logger = logger;
99 }
100
101 public Order PlaceOrder(CreateOrderRequest request)
102 {
103 // 純粹的業務邏輯,不含任何資料庫或基礎設施的細節
104 if (request.Total <= 0)
105 throw new ArgumentException("訂單金額必須大於零");
106 if (!request.Items.Any())
107 throw new ArgumentException("訂單至少需要一個品項");
108
109 var order = new Order
110 {
111 CustomerId = request.CustomerId,
112 Items = request.Items.ToList(),
113 Total = request.Total,
114 Status = OrderStatus.Pending,
115 CreatedAt = DateTime.UtcNow
116 };
117
118 _repo.Save(order);
119 _logger.LogInfo($"訂單 #{order.Id} 建立成功,金額:{order.Total:C}");
120
121 return order;
122 }
123}
DIP 與依賴注入(DI)的關係辨析
這兩個概念常常被混淆,但它們的層次不同:
- **DIP(依賴反轉原則)**是一個設計原則,它定義「目標」:依賴關係應指向抽象,高階模組不應直接依賴低階模組。
- **依賴注入(Dependency Injection,DI)**是一種設計模式,它定義「手段」:透過建構子、屬性或方法,從外部將依賴傳入類別,而非在類別內部自行建立。
在測試或小型程式中,DIP 可以不使用 DI Container 來實現,透過手動傳入依賴是沒有問題的;但 DI Container 讓 DIP 在包含幾十上百個類別的系統中得以輕鬆維護。
三種依賴注入方式的比較
1// ── 方式一:建構子注入(Constructor Injection)── 推薦
2// 優點:依賴關係一目了然、強制在建立物件時提供所有必要依賴、易於測試
3// 適用:必要的、核心的依賴
4public class OrderService
5{
6 private readonly IOrderRepository _repo;
7 private readonly ILogger _logger;
8
9 public OrderService(IOrderRepository repo, ILogger logger)
10 {
11 // 若任一依賴為 null,在物件建立時就立即失敗,而非在使用時才報錯
12 _repo = repo ?? throw new ArgumentNullException(nameof(repo));
13 _logger = logger ?? throw new ArgumentNullException(nameof(logger));
14 }
15}
16
17// ── 方式二:屬性注入(Property Injection)── 謹慎使用
18// 優點:允許選用依賴(有預設行為時才使用)
19// 缺點:依賴在物件建立後才注入,若忘記注入會在執行期才報錯
20// 適用:可選的附加功能,例如 Logger(不提供也能運作,只是不記錄日誌)
21public class OrderService
22{
23 private readonly IOrderRepository _repo;
24 public ILogger? Logger { get; set; } // 可選,若未注入就不記錄日誌
25
26 public OrderService(IOrderRepository repo)
27 => _repo = repo ?? throw new ArgumentNullException(nameof(repo));
28
29 public void PlaceOrder(Order order)
30 {
31 _repo.Save(order);
32 Logger?.LogInfo($"訂單 #{order.Id} 建立"); // 安全地使用可選 Logger
33 }
34}
35
36// ── 方式三:方法注入(Method Injection)── 少用
37// 優點:只有特定操作需要某個依賴時使用
38// 缺點:每次呼叫都需要傳入,呼叫端負擔較大
39// 適用:同一個物件在不同呼叫中使用不同的依賴實作
40public class ReportService
41{
42 public void GenerateReport(IOrderRepository repo, ReportType type)
43 {
44 // 同一個 ReportService 可以使用不同的 repo(例如讀取不同的資料庫)
45 var orders = repo.FindByCustomer(customerId: 1);
46 // 產生報表...
47 }
48}
重點回顧
- DIP 的核心:依賴關係應指向抽象(介面),而非具體的實作類別。
- 「反轉」的意義:由高階模組(業務邏輯)定義它所需要的介面,低階模組(技術實作)去實作這個介面;原本高階使用低階,現在低階反過來服務於高階定義的抽象。
- DIP 帶來的好處:可測試性(注入 Mock 物件)、可替換性(更換低階實作)、可維護性(業務邏輯與技術細節解耦)。
- DIP 是 SOLID 的「終章」,它整合了前四個原則的精神:SRP 劃清職責邊界讓抽象得以精準,OCP 利用這些抽象作為擴展點,LSP 確保多型替換的正確性,最終 DIP 把這一切串聯成一個低耦合、高彈性的架構。