SOLID 原則:介面隔離原則 (ISP)
8 分鐘
SOLID 原則:介面隔離原則 (ISP)
介面隔離原則 (Interface Segregation Principle,ISP) 是 SOLID 的第四條:
客戶端不應該被迫依賴它們不使用的介面。
簡單說:介面要夠精簡,不要把不相關的方法塞在同一個介面裡。
什麼是介面隔離原則
一個肥介面 (fat interface) 是指一個宣告了大量方法的介面,導致實作者被迫實作它們其實不需要的方法。
常見的症狀:
TypeScript
interface Printer {
print(document: string): void;
scan(): string;
fax(document: string, to: string): void;
copy(document: string): void;
}這個介面描述的是一台多功能事務機,但大多數設備其實只支援其中一部分。
肥介面的問題
TypeScript
// 這台簡易印表機只會列印、影印
class SimplePrinter implements Printer {
print(document: string): void {
console.log(`Printing: ${document}`);
}
copy(document: string): void {
console.log(`Copying: ${document}`);
}
// 這台機器根本沒有掃描功能,卻被迫實作
scan(): string {
throw new Error('此設備不支援掃描');
}
// 這台機器根本沒有傳真功能,卻被迫實作
fax(document: string, to: string): void {
throw new Error('此設備不支援傳真');
}
}SimplePrinter 被迫實作兩個它完全不支援的方法。這就是 ISP 的違反:類別被迫依賴它不需要的功能。
這還會導致 LSP 違反:第三方程式碼呼叫 printer.scan() 會突然得到一個例外。
拆分介面
將行為拆成獨立的小介面:
TypeScript
interface Printable {
print(document: string): void;
}
interface Scannable {
scan(): string;
}
interface Faxable {
fax(document: string, to: string): void;
}
interface Copyable {
copy(document: string): void;
}
// 簡單印表機:只實作它支援的功能
class SimplePrinter implements Printable, Copyable {
print(document: string): void {
console.log(`Printing: ${document}`);
}
copy(document: string): void {
console.log(`Copying: ${document}`);
}
}
// 多功能事務機:實作全部介面
class AllInOnePrinter implements Printable, Scannable, Faxable, Copyable {
print(document: string): void { /* ... */ }
scan(): string { return '...scanned content'; }
fax(document: string, to: string): void { /* ... */ }
copy(document: string): void { /* ... */ }
}現在 SimplePrinter 只需實作它真正支援的功能。任何需要掃描功能的程式碼,只接受 Scannable,不需要知道機器是否也支援列印或傳真。
實務應用
ISP 在前端開發中常見於 TypeScript 的型別設計:
TypeScript
// 職責分離清楚的小介面
interface Readable {
read(id: string): Promise<User>;
}
interface Writable {
create(data: UserInput): Promise<User>;
update(id: string, data: Partial<UserInput>): Promise<User>;
}
interface Deletable {
delete(id: string): Promise<void>;
}
// 此服務只需讀取權限
class ReadOnlyUserService implements Readable {
async read(id: string): Promise<User> {
return db.findUser(id);
}
}
// 此服務需要完整權限
class FullUserService implements Readable, Writable, Deletable {
async read(id: string): Promise<User> { /* ... */ }
async create(data: UserInput): Promise<User> { /* ... */ }
async update(id: string, data: Partial<UserInput>): Promise<User> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}總結
ISP 的實踐重點是:介面要小、要精、要專一。
遵守 ISP 有幾個好處:
- 實作者只需實作它真正需要的方法
- 不會有拋出異常的空實作 (避免 LSP 違反)
- 不同角色的程式碼只依賴它需要的功能,跟其他功能完全隔離
ISP 跟 LSP 密切相關:介面設計得夠精細,子類別就不會被迫實作不該有的方法。