今天我們來聊聊 SOLID 原則中的第四個原則 —— 介面隔離原則(Interface Segregation Principle, ISP)。這個原則由 Robert C. Martin(Uncle Bob)提出,主要是為了解決「胖介面」所帶來的問題。
介面隔離原則的核心思想是:
客戶端不應該被迫依賴它不使用的方法
換句話說,與其建立一個包含所有功能的大型介面,不如將其拆分為多個專門的小介面。每個客戶端只需要依賴它真正需要的介面,這樣可以降低耦合度並提高系統的靈活性。
值得一提的是,違反介面隔離原則會導致不必要的依賴關係。當一個介面包含太多方法時,實作該介面的類別就被迫要實作所有方法,即使其中某些方法對該類別來說毫無意義。這種設計會讓系統變得脆弱且難以維護。
接下來我們會透過一個多功能印表機系統的 TypeScript 範例,來了解如何避免臃腫的介面設計。
// 不好的範例:違反 ISP 的多功能設備介面
interface MultiFunctionDevice {
// 列印功能
print(document: string): void;
// 掃描功能
scan(): string;
// 傳真功能
fax(document: string, phoneNumber: string): void;
// 影印功能
copy(document: string): void;
// 電子郵件功能
sendEmail(document: string, email: string): void;
}
// 高階多功能印表機 - 可以實作所有功能
class PremiumPrinter implements MultiFunctionDevice {
print(document: string): void {
console.log(`列印文件: ${document}`);
}
scan(): string {
console.log("掃描文件中...");
return "掃描完成的文件內容";
}
fax(document: string, phoneNumber: string): void {
console.log(`傳真文件 "${document}" 到 ${phoneNumber}`);
}
copy(document: string): void {
console.log(`影印文件: ${document}`);
}
sendEmail(document: string, email: string): void {
console.log(`將文件 "${document}" 寄送至 ${email}`);
}
}
// 問題來了:基本印表機只需要列印功能
class BasicPrinter implements MultiFunctionDevice {
print(document: string): void {
console.log(`列印文件: ${document}`);
}
// 被迫實作不需要的功能,違反了 ISP!
scan(): string {
throw new Error("基本印表機不支援掃描功能");
}
fax(document: string, phoneNumber: string): void {
throw new Error("基本印表機不支援傳真功能");
}
copy(document: string): void {
throw new Error("基本印表機不支援影印功能");
}
sendEmail(document: string, email: string): void {
throw new Error("基本印表機不支援電子郵件功能");
}
}
// 掃描專用設備也有同樣的問題
class ScannerDevice implements MultiFunctionDevice {
print(document: string): void {
throw new Error("掃描器不支援列印功能");
}
scan(): string {
console.log("高解析度掃描中...");
return "高品質掃描文件";
}
// 又是一堆不需要的方法...
fax(document: string, phoneNumber: string): void {
throw new Error("掃描器不支援傳真功能");
}
copy(document: string): void {
throw new Error("掃描器不支援影印功能");
}
sendEmail(document: string, email: string): void {
throw new Error("掃描器不支援電子郵件功能");
}
}
// 使用範例 - 會產生問題
const devices: MultiFunctionDevice[] = [
new PremiumPrinter(),
new BasicPrinter(),
new ScannerDevice()
];
// 這樣的設計很危險,因為不是所有設備都支援所有功能
devices.forEach(device => {
try {
device.print("重要文件");
device.scan(); // BasicPrinter 會拋出例外
} catch (error) {
console.log(`錯誤: ${error.message}`);
}
});
上面的設計違反了介面隔離原則,主要問題包括:
// 修正範例:符合 ISP 的分離介面設計
// 將大介面拆分為專門的小介面
interface Printable {
print(document: string): void;
}
interface Scannable {
scan(): string;
}
interface Faxable {
fax(document: string, phoneNumber: string): void;
}
interface Copyable {
copy(document: string): void;
}
interface Emailable {
sendEmail(document: string, email: string): void;
}
// 基本印表機只實作需要的介面
class ImprovedBasicPrinter implements Printable {
print(document: string): void {
console.log(`基本印表機列印: ${document}`);
}
}
// 掃描專用設備只實作掃描介面
class ImprovedScannerDevice implements Scannable {
scan(): string {
console.log("專業掃描器掃描中...");
return "高解析度掃描結果";
}
}
// 多功能印表機可以實作多個介面
class ImprovedPremiumPrinter implements Printable, Scannable, Faxable, Copyable, Emailable {
print(document: string): void {
console.log(`高階印表機列印: ${document}`);
}
scan(): string {
console.log("高階印表機掃描中...");
return "掃描完成的文件內容";
}
fax(document: string, phoneNumber: string): void {
console.log(`傳真文件 "${document}" 到 ${phoneNumber}`);
}
copy(document: string): void {
console.log(`影印文件: ${document}`);
}
sendEmail(document: string, email: string): void {
console.log(`將文件 "${document}" 寄送至電子郵件`);
}
}
// 有些設備可能只支援部分功能的組合
class PrinterWithScanner implements Printable, Scannable {
print(document: string): void {
console.log(`多功能印表機列印: ${document}`);
}
scan(): string {
console.log("多功能印表機掃描中...");
return "掃描文件內容";
}
}
// 改進後的設備管理系統
class ImprovedDeviceManager {
// 只對支援列印的設備進行列印操作
printDocuments(printers: Printable[], documents: string[]): void {
console.log("=== 列印作業開始 ===");
printers.forEach((printer, index) => {
documents.forEach(doc => {
printer.print(`${doc} (設備 ${index + 1})`);
});
});
}
// 只對支援掃描的設備進行掃描操作
scanDocuments(scanners: Scannable[]): string[] {
console.log("=== 掃描作業開始 ===");
return scanners.map((scanner, index) => {
console.log(`使用設備 ${index + 1} 掃描:`);
return scanner.scan();
});
}
// 只對支援傳真的設備進行傳真操作
sendFaxes(faxMachines: Faxable[], document: string, phoneNumbers: string[]): void {
console.log("=== 傳真作業開始 ===");
faxMachines.forEach(faxMachine => {
phoneNumbers.forEach(phone => {
faxMachine.fax(document, phone);
});
});
}
}
// 使用範例 - 現在可以安全且彈性地運作
const deviceManager = new ImprovedDeviceManager();
// 建立各種設備
const basicPrinter = new ImprovedBasicPrinter();
const scanner = new ImprovedScannerDevice();
const premiumPrinter = new ImprovedPremiumPrinter();
const printerScanner = new PrinterWithScanner();
// 只有支援列印的設備會被用來列印
const printableDevices: Printable[] = [basicPrinter, premiumPrinter, printerScanner];
deviceManager.printDocuments(printableDevices, ["會議記錄", "報告書"]);
console.log("\n");
// 只有支援掃描的設備會被用來掃描
const scannableDevices: Scannable[] = [scanner, premiumPrinter, printerScanner];
const scanResults = deviceManager.scanDocuments(scannableDevices);
console.log("\n");
// 只有支援傳真的設備會被用來傳真
const faxableDevices: Faxable[] = [premiumPrinter]; // 只有高階印表機支援傳真
deviceManager.sendFaxes(faxableDevices, "緊急通知", ["02-1234-5678"]);
// 展示彈性:可以檢查設備是否支援特定功能
function demonstrateFlexibility(device: any) {
console.log("\n=== 設備功能檢測 ===");
if ('print' in device) {
console.log("此設備支援列印功能");
(device as Printable).print("測試列印");
}
if ('scan' in device) {
console.log("此設備支援掃描功能");
(device as Scannable).scan();
}
if ('fax' in device) {
console.log("此設備支援傳真功能");
} else {
console.log("此設備不支援傳真功能");
}
}
demonstrateFlexibility(basicPrinter);
demonstrateFlexibility(premiumPrinter);
這時候我們可以發現,改進後的設計讓每個設備只需要關心它真正支援的功能,大大降低了系統的複雜度和耦合度。
介面隔離原則的核心概念提醒我們:
需要注意的是,介面隔離原則與單一職責原則密切相關,但它們的關注點不同。單一職責原則關注類別的職責,而介面隔離原則關注介面的設計。
值得一提的是,在 TypeScript 中,介面隔離原則特別重要,因為 TypeScript 的結構性型別系統讓我們可以更靈活地組合介面。當我們遵循 ISP 時,可以充分利用 TypeScript 的型別推斷和聯合型別功能。
綜合以上所述,我們成功透過拆分介面實踐了介面隔離原則,讓系統中的每個元件只依賴它真正需要的功能,提高了系統的彈性和可維護性。