昨天的 SRP 讓每個類別、模組都只負責一件事。但職責分乾淨之後,還有一個問題沒解決:當系統要「新增」一項功能時,該怎麼加,才不會又把已經寫好、測過的程式碼弄壞?今天要談的開放封閉原則(OCP)與依賴反轉原則(DIP),就是在回答這個問題——而且這兩個原則其實是一體兩面。
定義:模組應該「對擴充開放(Open for Extension)」,同時「對修改封閉(Closed for Modification)」。換句話說,新增功能時應該透過「加入新的程式碼」來達成,而不是回頭去改動既有、已經穩定且經過測試的程式碼。
常見的實作手法:
定義:DIP 包含兩條規則:
容易混淆的地方:DIP 常常被跟依賴注入(DI)搞混,但兩者不是同一件事。DIP 是一種設計哲學,講的是「依賴關係該往哪個方向流動」;DI 只是實現這個原則的技術手法(透過工廠函式或框架,在執行時期把實作注入進去)。如果注入的東西本身還是一個具體的低層細節,而不是抽象介面,那即使用了 DI,也沒有真正做到依賴反轉——耦合依然存在,只是換了個地方發生。
延續 Day 15 的 LineNotificationService:目前訂單成立後只會發送 LINE 通知。如果現在要新增 Email 通知,沒有用抽象的做法,就得回頭修改呼叫端的程式碼,加一段 if/else 判斷要用哪種通知方式——這就違反了 OCP。
// notifications/notificationChannel.js —— 抽象介面(JS 沒有原生 interface,用約定的方法簽名表示)
class NotificationChannel {
notify(buyerId, message) {
throw new Error('必須實作 notify 方法');
}
}
// notifications/lineNotificationService.js —— 舊有實作,完全不用修改
class LineNotificationService extends NotificationChannel {
notify(buyerId, message) { /* 呼叫 LINE API */ }
}
// notifications/emailNotificationService.js —— 新增功能,只是「加」一個新類別
class EmailNotificationService extends NotificationChannel {
notify(buyerId, message) { /* 呼叫 Email API */ }
}
// orderService.js —— 高層模組只依賴抽象 NotificationChannel,不管背後是哪個實作
function createOrder(order, notifier /* NotificationChannel 的實例 */) {
const total = order.price * order.qty + order.shippingFee;
notifier.notify(order.buyerId, `訂單總額 ${total} 元`);
}
新增 EmailNotificationService 的過程中,LineNotificationService 跟 createOrder 一行都沒有被改到——這就是「對擴充開放、對修改封閉」。而 createOrder 從頭到尾只認得 NotificationChannel 這個抽象,不知道也不需要知道呼叫端傳進來的到底是 Line 還是 Email 的實作——這就是依賴反轉。
OCP 講的是「新增功能不要動舊程式碼」,而做到這件事的關鍵手法,正是讓高層模組依賴抽象而非具體實作——這正是 DIP 的核心。Day 15 的 SRP 讓每個模組只做一件事,Day 16 的 OCP 與 DIP 則讓這些各司其職的模組之間,透過抽象介面銜接,而不是直接綁死在對方的具體實作上——這樣未來要替換或新增實作時,才不會牽動整個系統。