過去兩天,抽象介面在 OCP、DIP 和工廠模式裡反覆出現。今天要把「依賴於介面,而非依賴於實作」這句設計模式領域最早提出的核心原則單獨拆開來講清楚——它其實是前兩天所有做法背後共同的理由。
定義:介面(Interface)定義的是「能做什麼」——也就是方法名稱、輸入輸出的約定;實作(Implementation)則是「具體怎麼做到」的程式碼細節。依賴介面而非實作,意味著呼叫端只需要知道對方「能做什麼」,完全不需要知道對方內部「怎麼做到」。
延續 Day 16、17 的通知範例,這次換一個新的場景:訂單資料的儲存。如果 OrderService 直接依賴具體的 MySQLOrderRepository,未來想把資料庫換成 PostgreSQL,或是在測試時想用記憶體暫存的假資料,都得回頭去改 OrderService 本身的程式碼。
// repositories/orderRepository.js —— 定義介面(用約定的方法簽名表示)
class OrderRepository {
save(order) { throw new Error('必須實作 save 方法'); }
findById(id) { throw new Error('必須實作 findById 方法'); }
}
// repositories/mysqlOrderRepository.js —— 正式環境的實作
class MySQLOrderRepository extends OrderRepository {
save(order) { /* 寫入 MySQL */ }
findById(id) { /* 查詢 MySQL */ }
}
// repositories/inMemoryOrderRepository.js —— 測試用的假實作,同樣符合介面
class InMemoryOrderRepository extends OrderRepository {
#orders = new Map();
save(order) { this.#orders.set(order.id, order); }
findById(id) { return this.#orders.get(id); }
}
// services/orderService.js —— 只依賴抽象的 OrderRepository,不管背後是哪個實作
class OrderService {
constructor(orderRepository /* OrderRepository 的實例 */) {
this.orderRepository = orderRepository;
}
createOrder(input) {
const order = { ...input, total: input.price * input.quantity + input.shippingFee };
this.orderRepository.save(order);
return order;
}
}
// 正式環境
const service = new OrderService(new MySQLOrderRepository());
// 測試環境:不用真的連資料庫,一樣可以測 OrderService 的邏輯
const testService = new OrderService(new InMemoryOrderRepository());
OrderService 的建構子只認得 OrderRepository 這個抽象約定,不管實際傳進來的是連著真實資料庫的 MySQLOrderRepository,還是測試用的 InMemoryOrderRepository,createOrder 這個方法裡的邏輯完全不用改一行。
「依賴介面而非實作」不是一個全新的原則,而是把 Day 16 的 DIP 和 Day 17 的工廠模式背後共同的精神講得更直白——呼叫端只跟「約定」打交道,實際的實作可以隨時替換,不管是為了換技術、換廠商,還是單純為了測試,系統都不需要因此傷筋動骨。