iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
自我挑戰組

程式碼門診:診斷壞味道、開出重構處方系列 第 10 篇

依賴抽象而非具體實作 - 依賴倒置原則 (Dependency Inversion Principle)

  • 分享至 

  • xImage
  •  

簡單介紹

今天我們來聊 SOLID 原則的最後一個原則 —— 依賴倒置原則(Dependency Inversion Principle, DIP)。這個原則是由 Robert C. Martin 提出,是物件導向設計中非常重要的概念,也是實現鬆耦合架構的關鍵。

依賴倒置原則包含兩個核心概念:

  1. 高層模組不應該依賴於低層模組,兩者都應該依賴抽象
  2. 抽象不應該依賴細節,細節應該依賴抽象

換句話說,我們應該依賴介面或抽象類別,而不是具體的實作類別。這個原則能讓我們的程式碼更加彈性、可測試,並且容易維護。

值得一提的是,這個原則之所以叫做「倒置」,是因為它顛覆了傳統的依賴關係。在傳統設計中,高層模組通常直接依賴低層模組,但透過依賴倒置原則,我們讓高層和低層模組都依賴抽象,從而實現了依賴關係的倒置。

接下來我們會透過一個用戶通知系統的 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 方法
// 這違反了開放封閉原則!

問題分析

上面的設計違反了依賴倒置原則,主要問題包括:

  1. 緊耦合:高層模組直接依賴具體的低層模組實作
  2. 難以測試:無法輕易替換依賴進行單元測試
  3. 難以擴展:新增新的通知方式需要修改現有程式碼
  4. 違反開放封閉原則:對修改開放,對擴展封閉

符合依賴倒置原則(修正):依賴抽象介面,使用依賴注入

// 修正範例:符合 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();

改進重點說明

  1. 依賴抽象:ImprovedUserNotificationManager 依賴 NotificationService 介面而非具體實作
  2. 依賴注入:透過建構子注入依賴,提高了可測試性和彈性
  3. 策略模式:使用策略模式管理不同的通知服務
  4. 開放封閉原則:新增新的通知方式不需要修改現有程式碼
  5. 單一職責:每個類別都有明確的單一職責

這時候我們可以發現,當我們需要新增 Slack 通知服務時,完全不需要修改 ImprovedUserNotificationManager 的任何程式碼,只需要:

  1. 建立新的 SlackNotificationService 實作 NotificationService 介面
  2. 在工廠中註冊新策略

這就是依賴倒置原則的威力!

總結

依賴倒置原則的核心概念提醒我們:

  1. 依賴抽象而非具體:高層模組應該依賴介面或抽象類別
  2. 使用依賴注入:透過建構子、setter 或介面注入依賴
  3. 倒置依賴關係:讓具體實作依賴抽象,而不是相反
  4. 提高可測試性:透過注入 mock 物件進行單元測試

需要注意的是,依賴倒置原則是實現鬆耦合架構的關鍵。當我們遵循這個原則時,系統的各個模組之間的耦合度大大降低,提高了系統的彈性和可維護性。

值得一提的是,依賴倒置原則與控制反轉(Inversion of Control, IoC)密切相關。在現代的 TypeScript 開發中,我們可以使用像 InversifyJS 這樣的依賴注入容器來更好地實踐這個原則。

綜合以上所述,我們成功透過依賴抽象和依賴注入實踐了依賴倒置原則,讓系統變得更加彈性、可測試且容易維護。這樣的設計讓我們能夠輕鬆應對需求變更,並且保持程式碼的清潔和穩定。

參考資料

上一篇
不要強迫依賴不需要的功能 - 介面隔離原則 (Interface Segregation Principle)
下一篇
別再蓋家族大樓,改玩樂高 - 組合優於繼承 (Composition over Inheritance)
系列文
程式碼門診:診斷壞味道、開出重構處方 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言