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 把這一切串聯成一個低耦合、高彈性的架構。