今天我們來聊 SOLID 原則的最後一個原則 —— 依賴倒置原則(Dependency Inversion Principle, DIP)。這個原則是由 Robert C. Martin 提出,是物件導向設計中非常重要的概念,也是實現鬆耦合架構的關鍵。
依賴倒置原則包含兩個核心概念:
換句話說,我們應該依賴介面或抽象類別,而不是具體的實作類別。這個原則能讓我們的程式碼更加彈性、可測試,並且容易維護。
值得一提的是,這個原則之所以叫做「倒置」,是因為它顛覆了傳統的依賴關係。在傳統設計中,高層模組通常直接依賴低層模組,但透過依賴倒置原則,我們讓高層和低層模組都依賴抽象,從而實現了依賴關係的倒置。
接下來我們會透過一個用戶通知系統的 TypeScript 範例,來了解如何實踐這個重要的設計原則。
// 不好的範例:違反 DIP 的通知系統設計
// 低層模組:具體的通知服務實作
class EmailService {
sendEmail(recipient: string, subject: string, message: string): void {
console.log(`發送電子郵件給 ${recipient}`);
console.log(`主旨:${subject}`);
console.log(`內容:${message}`);
console.log("電子郵件發送完成\n");
}
}
class SmsService {
sendSms(phoneNumber: string, message: string): void {
console.log(`發送簡訊到 ${phoneNumber}`);
console.log(`內容:${message}`);
console.log("簡訊發送完成\n");
}
}
class PushNotificationService {
sendPushNotification(deviceId: string, title: string, message: string): void {
console.log(`發送推播通知到設備 ${deviceId}`);
console.log(`標題:${title}`);
console.log(`內容:${message}`);
console.log("推播通知發送完成\n");
}
}
// 高層模組:用戶通知管理器,直接依賴具體的低層模組
class UserNotificationManager {
private emailService: EmailService; // 直接依賴具體實作!
private smsService: SmsService; // 違反 DIP!
private pushService: PushNotificationService; // 緊耦合!
constructor() {
// 在構造函式中直接建立具體實例
this.emailService = new EmailService();
this.smsService = new SmsService();
this.pushService = new PushNotificationService();
}
notifyUser(userId: string, message: string, notificationType: string): void {
// 需要知道所有具體實作的細節
if (notificationType === "email") {
// 假設我們有方法取得用戶 email
const email = this.getUserEmail(userId);
this.emailService.sendEmail(email, "系統通知", message);
} else if (notificationType === "sms") {
const phone = this.getUserPhone(userId);
this.smsService.sendSms(phone, message);
} else if (notificationType === "push") {
const deviceId = this.getUserDeviceId(userId);
this.pushService.sendPushNotification(deviceId, "通知", message);
}
// 如果要新增新的通知方式,就必須修改這個方法!
}
// 模擬取得用戶資訊的方法
private getUserEmail(userId: string): string {
return `user${userId}@example.com`;
}
private getUserPhone(userId: string): string {
return `0912-345-${userId}`;
}
private getUserDeviceId(userId: string): string {
return `device_${userId}`;
}
}
// 使用範例 - 問題重重
const notificationManager = new UserNotificationManager();
// 每種通知類型都需要硬編碼的字符串
notificationManager.notifyUser("001", "您有新訊息", "email");
notificationManager.notifyUser("002", "系統維護通知", "sms");
notificationManager.notifyUser("003", "新功能上線", "push");
// 如果想要新增 Slack 通知,就必須:
// 1. 修改 UserNotificationManager 類別
// 2. 新增 SlackService 的依賴
// 3. 修改 notifyUser 方法
// 這違反了開放封閉原則!
上面的設計違反了依賴倒置原則,主要問題包括:
// 修正範例:符合 DIP 的通知系統設計
// 定義抽象介面(高層和低層模組都依賴這個抽象)
interface NotificationService {
send(recipient: string, message: string, metadata?: any): Promise<boolean>;
getServiceType(): string;
}
// 定義用戶資訊介面
interface User {
id: string;
email: string;
phone: string;
deviceId: string;
}
// 低層模組:具體實作依賴抽象介面
class ImprovedEmailService implements NotificationService {
async send(recipient: string, message: string, metadata?: any): Promise<boolean> {
const subject = metadata?.subject || "系統通知";
console.log(`發送電子郵件給 ${recipient}`);
console.log(`主旨:${subject}`);
console.log(`內容:${message}`);
console.log("電子郵件發送完成\n");
return true;
}
getServiceType(): string {
return "Email";
}
}
class ImprovedSmsService implements NotificationService {
async send(recipient: string, message: string, metadata?: any): Promise<boolean> {
console.log(`發送簡訊到 ${recipient}`);
console.log(`內容:${message}`);
console.log("簡訊發送完成\n");
return true;
}
getServiceType(): string {
return "SMS";
}
}
class ImprovedPushNotificationService implements NotificationService {
async send(recipient: string, message: string, metadata?: any): Promise<boolean> {
const title = metadata?.title || "通知";
console.log(`發送推播通知到設備 ${recipient}`);
console.log(`標題:${title}`);
console.log(`內容:${message}`);
console.log("推播通知發送完成\n");
return true;
}
getServiceType(): string {
return "Push Notification";
}
}
// 新增的通知服務 - 不需要修改現有程式碼!
class SlackNotificationService implements NotificationService {
async send(recipient: string, message: string, metadata?: any): Promise<boolean> {
const channel = metadata?.channel || "#general";
console.log(`發送 Slack 訊息到 ${channel}`);
console.log(`提及用戶:@${recipient}`);
console.log(`內容:${message}`);
console.log("Slack 訊息發送完成\n");
return true;
}
getServiceType(): string {
return "Slack";
}
}
// 用戶服務介面
interface UserService {
getUser(userId: string): User | null;
}
// 用戶服務實作
class UserServiceImpl implements UserService {
private users: Map<string, User> = new Map([
["001", { id: "001", email: "user001@example.com", phone: "0912-345-001", deviceId: "device_001" }],
["002", { id: "002", email: "user002@example.com", phone: "0912-345-002", deviceId: "device_002" }],
["003", { id: "003", email: "user003@example.com", phone: "0912-345-003", deviceId: "device_003" }]
]);
getUser(userId: string): User | null {
return this.users.get(userId) || null;
}
}
// 通知策略工廠
class NotificationFactory {
private strategies: Map<string, NotificationService> = new Map();
registerStrategy(type: string, service: NotificationService): void {
this.strategies.set(type, service);
}
getStrategy(type: string): NotificationService | null {
return this.strategies.get(type) || null;
}
getAllStrategies(): string[] {
return Array.from(this.strategies.keys());
}
}
// 高層模組:現在依賴抽象而非具體實作
class ImprovedUserNotificationManager {
private notificationFactory: NotificationFactory;
private userService: UserService;
// 依賴注入:注入抽象介面而非具體實作
constructor(notificationFactory: NotificationFactory, userService: UserService) {
this.notificationFactory = notificationFactory;
this.userService = userService;
}
async notifyUser(userId: string, message: string, notificationType: string): Promise<boolean> {
try {
// 獲取通知策略
const notificationService = this.notificationFactory.getStrategy(notificationType);
if (!notificationService) {
console.log(`不支援的通知類型:${notificationType}`);
return false;
}
// 獲取用戶資訊
const user = this.userService.getUser(userId);
if (!user) {
console.log(`找不到用戶:${userId}`);
return false;
}
// 根據通知類型決定收件人
const recipient = this.getRecipient(user, notificationType);
const metadata = this.getMetadata(notificationType);
// 發送通知(不需要知道具體實作細節)
console.log(`使用 ${notificationService.getServiceType()} 服務發送通知:`);
return await notificationService.send(recipient, message, metadata);
} catch (error) {
console.log(`通知發送失敗:${error.message}`);
return false;
}
}
async notifyUserMultipleChannels(userId: string, message: string, channels: string[]): Promise<void> {
console.log(`=== 多通道通知用戶 ${userId} ===`);
const results = await Promise.all(
channels.map(channel => this.notifyUser(userId, message, channel))
);
const successCount = results.filter(result => result).length;
console.log(`通知結果:${successCount}/${channels.length} 個通道成功發送\n`);
}
getSupportedNotificationTypes(): string[] {
return this.notificationFactory.getAllStrategies();
}
private getRecipient(user: User, notificationType: string): string {
switch (notificationType) {
case "email": return user.email;
case "sms": return user.phone;
case "push": return user.deviceId;
case "slack": return user.id; // 使用用戶 ID 作為 Slack 用戶名
default: return user.email;
}
}
private getMetadata(notificationType: string): any {
switch (notificationType) {
case "email": return { subject: "重要通知" };
case "push": return { title: "系統訊息" };
case "slack": return { channel: "#notifications" };
default: return {};
}
}
}
// 使用範例 - 展示依賴注入和策略模式的威力
console.log("=== 設定依賴注入 ===");
// 建立具體服務實例
const emailService = new ImprovedEmailService();
const smsService = new ImprovedSmsService();
const pushService = new ImprovedPushNotificationService();
const slackService = new SlackNotificationService(); // 新服務!
// 建立工廠並註冊策略
const factory = new NotificationFactory();
factory.registerStrategy("email", emailService);
factory.registerStrategy("sms", smsService);
factory.registerStrategy("push", pushService);
factory.registerStrategy("slack", slackService); // 輕鬆新增新服務!
// 建立用戶服務
const userService = new UserServiceImpl();
// 建立通知管理器(依賴注入)
const improvedManager = new ImprovedUserNotificationManager(factory, userService);
// 展示支援的通知類型
console.log("支援的通知類型:", improvedManager.getSupportedNotificationTypes());
console.log();
// 使用範例
async function demonstrateNotifications() {
// 單一通道通知
await improvedManager.notifyUser("001", "您有新的訂單", "email");
await improvedManager.notifyUser("002", "系統將於今晚維護", "sms");
await improvedManager.notifyUser("003", "新功能已上線", "push");
await improvedManager.notifyUser("001", "會議提醒", "slack");
// 多通道通知
await improvedManager.notifyUserMultipleChannels(
"002",
"重要安全更新",
["email", "sms", "push"]
);
}
demonstrateNotifications();
ImprovedUserNotificationManager 依賴 NotificationService 介面而非具體實作這時候我們可以發現,當我們需要新增 Slack 通知服務時,完全不需要修改 ImprovedUserNotificationManager 的任何程式碼,只需要:
SlackNotificationService 實作 NotificationService 介面這就是依賴倒置原則的威力!
依賴倒置原則的核心概念提醒我們:
需要注意的是,依賴倒置原則是實現鬆耦合架構的關鍵。當我們遵循這個原則時,系統的各個模組之間的耦合度大大降低,提高了系統的彈性和可維護性。
值得一提的是,依賴倒置原則與控制反轉(Inversion of Control, IoC)密切相關。在現代的 TypeScript 開發中,我們可以使用像 InversifyJS 這樣的依賴注入容器來更好地實踐這個原則。
綜合以上所述,我們成功透過依賴抽象和依賴注入實踐了依賴倒置原則,讓系統變得更加彈性、可測試且容易維護。這樣的設計讓我們能夠輕鬆應對需求變更,並且保持程式碼的清潔和穩定。