什麼是介面隔離原則 (ISP)?
介面隔離原則 (Interface Segregation Principle, ISP) 是物件導向設計 SOLID 原則中的第四項,其核心理念是:客戶端不應該被迫依賴於它們不使用的方法。在實務開發中,這意謂著我們應該將龐大、臃腫的介面拆分為多個更小、更具體且專注於特定職責的小介面。這樣做的優點在於能顯著降低模組間的「非必要耦合」,當小介面發生變動時,只有真正使用到該介面的模組才需要重新編譯或修改。ISP 能確保系統具備高品質的內聚力,使程式碼更易於理解、測試與擴充,是構建強韌軟體架構的重要基石。
在之前的文章中有提出了當 重複的東西一再出現的問題,當時並沒有特別的去談一些理論和解決方法,但在後面幾天小編開始慢慢的置入 SOLID 原則 中的:
今天想來開箱另外一個設計程式時的 介面隔離原則 (Interface Segregation Principle, ISP),談談透過介面的隔離來確保高內聚性 (Cohesion) 和低耦合性 (Coupling),使程式碼易於理解、擴充和維護。這份 ISP 教學 將幫助您優化系統的 軟體架構設計。
介面隔離原則 (ISP):縮小介面以減少依賴
讓我們複習一下之前提到的情境描述。假設我們正在開發一個角色扮演遊戲 (RPG) 的程式,其中有不同類型的玩家,包括基本玩家和進階玩家。每個玩家類型都有自己的檢查條件,程式碼重構 的目標是確保系統易於擴充。
為什麼我們需要介面隔離?
在沒有應用 ISP 之前,我們可能會試圖建立一個「萬能玩家介面」,強迫所有角色實作所有的屬性與方法。例如,如果一個「平民」角色被強迫實作 attack() 方法,即使他根本不會攻擊,這就違反了介面隔離原則。當一個類別被迫依賴它不使用的方法時,系統就會變得脆弱且難以維護。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const player1 = { name: "玩家1", level: 1, health: 100, damage: 10, };
const player2 = { name: "玩家2", level: 5, health: 150, damage: 15, agility: 20, inventory: ["劍", "盾"], };
function processPlayerData( name, level, health, damage, agility, inventory, statusCode ) { if ( health > 10 && (name === "foo" || damage < 5) && (name !== "bar" || agility > 20) ) { } if (statusCode === 20100) { }
if (statusCode === 20101) { } }
|
介面定義:分離基本與進階角色邏輯
首先定義了 Player 的介面,包含了所有玩家類型都需要具備的通用屬性。透過 介面隔離,我們可以確保每個類別都只依賴於它實際需要的方法。
但由於角色的不同,改善的方向應該是把角色分開,這樣每個角色都只會用到自己需要的屬性和方法。
- 基本玩家: 透過
createPlayer 函數實現了 Player 介面,確保了基本玩家包含了通用的屬性遵循了 Player 介面。 - 進階玩家: 透過
createAdvancedPlayer 函數擴充 Player 介面,確保了進階玩家不僅遵循了 Player 介面也包含了進階玩家特有的屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| function createPlayer(playerData) { return { ...playerData, }; }
const player1 = createPlayer({ name: "玩家1", level: 1, health: 100, damage: 10, });
function createAdvancedPlayer(advancedPlayerData) { return { ...createPlayer(advancedPlayerData), }; }
const player2 = createAdvancedPlayer({ name: "玩家2", level: 5, health: 150, damage: 15, agility: 20, inventory: ["劍", "盾"], });
function processPlayerData(player) { const { name, health, damage, agility } = player; if ( health > 10 && (name === "foo" || damage < 5) && (name !== "bar" || agility > 20) ) { } if (statusCode === 20100) { }
if (statusCode === 20101) { } }
|
通用處理函數
處理通用處理函數的部分,這裡建立了 processPlayerData 函數,可以接受任何類型的玩家物件作為參數,目標是不直接依賴於特定的玩家類型讓函數更具通用性,方便未來新增更多的角色。
判斷的部份選擇把不同功能的判斷用 function 拆開,將這些條件拆分成獨立的函數可以提高程式碼的可讀性和維護性,這樣做讓每個檢查條件都有自己的名稱,更容易理解和測試。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function isHealthy(player) { return player.health > 10; }
function hasDesiredNameOrLowDamage(player) { return player.name === "foo" || player.damage < 5; }
function hasDesiredNameOrHighAgility(player) { return player.name !== "bar" || player.agility > 20; }
function processPlayerData(player) { if ( isHealthy(player) && hasDesiredNameOrLowDamage(player) && hasDesiredNameOrHighAgility(player) ) { } if (statusCode === 20100) { }
if (statusCode === 20101) { } }
|
重構完之後發現,只有 AdvancedPlayer 才需要有 hasDesiredNameOrHighAgility,所以再把腳色拆分進行重構,讓每個玩家類型都分別具有自己的檢查邏輯,基本玩家和進階玩家分別知道如何檢查自己的條件,這就是介面隔離。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function isHealthy(player) { return player.health > 10; }
function hasDesiredNameOrLowDamage(player) { return player.name === "foo" || player.damage < 5; }
function hasDesiredNameOrHighAgility(player) { return player.name !== "bar" || player.agility > 20; }
function createPlayer(playerData) { const { name, health, damage } = playerData; return { name, health, damage,
checkPlayerCondition() { return isHealthy({ health, name, damage }) && hasDesiredNameOrLowDamage({ name, damage }); }, }; }
function createAdvancedPlayer(advancedPlayerData) { const { name, health, damage, agility } = advancedPlayerData; const player = createPlayer({ name, health, damage }); return { ...player, agility,
checkAdvancedPlayerCondition() { return hasDesiredNameOrHighAgility({ name: player.name, agility }); }, }; }
|
ISP vs SRP:職責與介面的微妙平衡
很多人會混淆 單一職責原則 (SRP) 與 介面隔離原則 (ISP)。雖然兩者都強調「精簡」,但關注點不同:
- SRP 關注的是類別的「內部實現」:一個類別應該只有一個引起它變化的原因。
- ISP 關注的是類別的「外部暴露」:客戶端不應該被迫依賴於它們不使用的方法。
在我們的 RPG 案例中,即使一個 AdvancedPlayer 類別符合 SRP,如果我們強迫一個只需要讀取 name 的顯示元件去依賴整個包含 checkAdvancedPlayerCondition 的大介面,那就是違反了 ISP。這就是為什麼進行 軟體架構設計 時,必須精確拆分介面。
通用函數設計:基於介面的處理與重構
這裡建立了 processPlayerData 函數,目標是不直接依賴於特定的玩家類型。這種 軟體架構設計 讓函數更具通用性,方便未來新增更多的角色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function processPlayerData(player) { if (player.checkPlayerCondition()) { console.log(`${player.name} 符合基本條件`); }
if ( player?.checkAdvancedPlayerCondition && player?.checkAdvancedPlayerCondition() ) { console.log(`${player.name} 符合進階條件`); }
if (statusCode === 20100) { }
if (statusCode === 20101) { } }
const player1 = createPlayer({ name: "玩家1", health: 100, damage: 10, }); const player2 = createAdvancedPlayer({ name: "玩家2", health: 150, damage: 15, agility: 20, });
processPlayerData(player1);
processPlayerData(player2);
|
為什麼 ISP 對大型專案至關重要?
在大型專案中,介面隔離原則 (ISP) 能顯著減少「編譯依賴性」。當你修改一個龐大介面中的其中一個方法時,所有依賴該介面的模組可能都需要重新編譯或重新部署。透過將大介面拆分為多個小介面,你可以:
- 降低耦合性:模組之間的連動減少,修改程式碼的風險降低。
- 提高程式碼重用性:小介面更容易被不同的類別實作。
- 增強可測試性:在撰寫單元測試時,Mock 物件會變得更加簡單,因為你只需要實作測試所需的少量方法。
processPlayerData 函數最後不需要知道你是哪種特定的玩家類型,只關心玩家是否符合介面定義。這就是 介面隔離原則 (ISP) 的具體應用,有助於確保程式碼的結構清晰,並簡化依賴關係,同時支持未來的靈活擴充。不論是在簡單的腳本還是複雜的 SOLID 原則 實作中,ISP 都是不可或缺的基石。
FAQ:介面隔離原則常見問題
Q1:如果介面拆得太細,會不會導致類別爆炸?
A:這是一個權衡。過度拆分(介面碎片化)確實會增加系統的認知負擔。建議的標準是:觀察是否有客戶端因為介面變動而頻繁進行「不必要」的重新測試或部署。如果有,那就該拆;如果各方法高度相關且幾乎總是被同時使用,則應保留在同一個介面中。
Q2:ISP 只能應用在有 Interface 關鍵字的語言(如 Java/TS)嗎?
A:並非如此。 在 JavaScript 這類動態語言中,ISP 體現於「參數的預期」。例如:一個函數如果接收一個龐大的 options 物件,但只讀取其中的一個屬性,這在廣義上也是違反 ISP 的。改為只傳入所需的屬性,就是一種介面隔離的實踐。
Q3:ISP 如何協助前端元件開發?
A:在 React 中,這體現於 Props 的設計。避免傳遞整個龐大的 Data Object 給子元件,而是只傳遞元件所需的最小屬性。這能避免子元件因為無關資料的變動而產生不必要的重新渲染,提升效能與可測試性。
掌握了 介面隔離原則,您就能為軟體架構注入更高品質的內聚力。持續在重構中實踐「最小依賴」,讓您的程式碼更優雅、更具靈活性!
更多相關文章
想建構具備高擴充性的軟體系統嗎?本篇指南深入探討 Domain-Driven Design (DDD) 與 API-First 開發策略。我們將解析如何建立領域模型、運用通用語言並落實開放封閉原則 (OCP)。前 150 字直接回答 DDD 與 API-First 的核心定義,助您以業務邏輯為中心,打造穩定且靈活的現代化架構。
大家都說需要,就真的需要嗎?本篇指南從需求管理出發,以 React 的 Redux 架構為例,深入解析技術選型的權衡。我們將分析全域狀態管理的開發負擔,並對比傳統 Redux、Redux Toolkit 與原生 Hooks 的適用場景。前 150 字直接回答選型核心,助您做出更明智的系統設計決策。
有重複程式碼就是壞味道嗎?本篇指南從主觀價值出發,探討程式架構中的「好與壞」。我們將解析 Shotgun Surgery (散彈槍式修改)、Divergent Change (發散式修改) 以及內聚與耦合的核心定義。前 150 字直接回答程式碼異味的本質,助您在靈活擴展與集中管理之間找到最佳平衡點。
您是否正深受「隕石需求」與交付期限的壓力?本篇指南探討時間限制對開發效率的影響。我們將從《人月神話》出發,分析「隕石式開發」與「敏捷開發」的應對之道。前 150 字直接回答時間管理的生存核心,助您在資源有限的情況下透過優先順序與自動化測試,達成高品質交付。
擁有過多物品不一定快樂,軟體開發也是如此。本篇指南探討軟體開發中的六大核心成本:流程、理解、修改、執行、測試與技術成本。前 150 字直接回答開發成本的本質,助您在有限資源下做出智慧抉擇,透過斷捨離與自動化,打造高效且易維護的系統架構。
喜歡這篇文章,請幫忙拍拍手喔 🤣