昨天談到 OCP 和 DIP,重點都放在「程式碼之間該怎麼依賴抽象」這個概念層次。今天要往下一層,看實際的程式碼要怎麼被組織成一個個可以獨立替換、匯入匯出的單位——也就是模組化的具體做法。
定義:在 ES Modules 之下,每一個檔案本身就是一個獨立的作用域(scope)。沒有被明確 export 的內容,預設就是這個檔案的私有內容,不會外流;需要對外開放的部分,透過 import / export 明確宣告。
特點:
import 語句在編譯時期就能被分析出來,讓工具可以做 tree-shaking(把用不到的程式碼移除)。定義:在 ES Modules 普及之前,為了避免把太多變數和函式直接暴露在全域環境,開發者會把相關的功能包在同一個物件底下,模擬出「分類」的效果:
const OrderUtils = {
calculateTotal(price, qty, shippingFee) {
return price * qty + shippingFee;
},
applyDiscount(total, rate) {
return total * (1 - rate);
}
};
現在 ES Modules 本身就提供了檔案級別的隔離,命名空間模式的必要性已經大幅降低。但當需要把一群邏輯相關的函式,包成一個看起來像「工具箱」的群組時,這種寫法仍然是一種清楚、可讀的組織方式。
定義:用一個專門的函式(工廠函式)來負責「建立物件」這件事,呼叫端不需要知道物件是怎麼被建構出來的,只需要呼叫工廠函式、取得一個現成的實例即可。
目的:把「該建立哪一種具體實作」的判斷邏輯,集中在一個地方,其餘的呼叫端只需要面對抽象的結果,不用各自重複判斷。
延續昨天 NotificationChannel 的例子:如果要根據代購業者自己的設定,動態決定該用 Line 還是 Email 通知,呼叫端不應該自己寫一段 if/else 去判斷,而是把「決定用哪一種」的邏輯,交給一個工廠函式集中處理。
// notifications/lineNotificationService.js —— 各自獨立的模組
export class LineNotificationService extends NotificationChannel {
notify(buyerId, message) { /* 呼叫 LINE API */ }
}
// notifications/emailNotificationService.js
export class EmailNotificationService extends NotificationChannel {
notify(buyerId, message) { /* 呼叫 Email API */ }
}
// notifications/notificationFactory.js —— 工廠函式集中決定「要建立哪一種」
import { LineNotificationService } from './lineNotificationService.js';
import { EmailNotificationService } from './emailNotificationService.js';
export function createNotifier(channel) {
if (channel === 'line') return new LineNotificationService();
if (channel === 'email') return new EmailNotificationService();
throw new Error(`未知的通知管道:${channel}`);
}
// orderService.js —— 呼叫端只需要一行,不管背後實際建立的是哪個 class
import { createNotifier } from './notifications/notificationFactory.js';
const notifier = createNotifier(merchant.preferredChannel);
notifier.notify(order.buyerId, `訂單總額 ${total} 元`);
如果之後又要新增第三種通知管道,只需要在 notificationFactory.js 裡多加一行判斷、新增一個對應的檔案,orderService.js 完全不用被碰到——這跟昨天 OCP 講的「對擴充開放、對修改封閉」是同一件事,只是這次發生在「物件怎麼被建立」這個環節。
ES Modules 負責的是「檔案層級的封裝」,工廠模式負責的則是「物件建立邏輯的封裝」——兩者搭配起來,呼叫端拿到的永遠是一個符合抽象介面的實例,不需要知道實際上是哪個檔案、哪個 class 在背後運作。這正是昨天 DIP 講的「依賴抽象」,在真實的檔案結構與程式碼組織上的具體落實。