iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0

在之前的文章中有提出了當重複的東西一再出現的問題,當時並沒有特別的去談一些理論和解決方法,但在後面幾天小編開始慢慢的置入 SOLID 中的

今天想來開箱另外一個設計程式時的介面隔離原則,談談透過介面的隔離來確保高內聚性 (Cohesion) 和低耦合性 (Coupling),使程式碼易於理解、擴充和維護,一起來看看有沒有機會解決上次的問題吧。

Interface Segregation Principle

讓我們複習一下之前提到的情境描述,假設我們正在開發一個角色扮演遊戲 (RPG) 的程式,其中有不同類型的玩家,包括基本玩家和進階玩家,每個玩家類型都有自己的檢查條件,重構的目標是希望確保程式碼易於擴充,以應對未來可能新增的玩家類型。

// 拆開
// 初始版本只有基本角色屬性
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 介面也包含了進階玩家特有的屬性
// 定義一個函數,用於建立玩家物件
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 拆開,將這些條件拆分成獨立的函數可以提高程式碼的可讀性和維護性,這樣做讓每個檢查條件都有自己的名稱,更容易理解和測試。

// 定義一個函數,用於檢查玩家的健康狀態
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,所以再把腳色拆分進行重構,讓每個玩家類型都分別具有自己的檢查邏輯,基本玩家和進階玩家分別知道如何檢查自己的條件,這就是介面隔離。

// 定義一個函數,用於檢查玩家的健康狀態
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(player) && hasDesiredNameOrLowDamage(player);
    },
  };
}

// 定義進階玩家函數,接受包含進階玩家屬性的物件作為參數
function createAdvancedPlayer(advancedPlayerData) {
  const { name, health, damage, agility } = advancedPlayerData;
  const player = createPlayer({ name, health, damage });
  return {
    ...player,
    agility,

    // 進階玩家特有的檢查函數
    checkAdvancedPlayerCondition() {
      return hasDesiredNameOrHighAgility(player);
    },
  };
}

// 通用的處理玩家資料函數,接受任何類型的玩家物件作為參數
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);
// 輸出:玩家1 符合基本條件
processPlayerData(player2);
// 輸出:玩家2 符合基本條件
// 輸出:玩家2 符合進階條件

processPlayerData 函數最後不需要知道你是哪種特定的玩家類型,只關心玩家是否符合 Player 介面,如果未來新增其他類型的玩家,我們只需透過 Player 介面建立一個新的玩家類別,而不需要修改現有的程式碼。

透過這樣的設計降低了不同角色之間的耦合性,這就是介面隔離原則 (Interface Segregation Principle),介面隔離原則有助於確保程式碼的結構清晰,並使不同部分之間的依賴關係簡化,同時支持未來的擴充和修改。


上一篇
決定成敗的系統細節分析
下一篇
影分身之術 X 里氏替換原則
系列文
前端三分鐘 X 每天三分鐘的斷捨離,讓每一天都可以早點下班30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言