把自己角色扮演好 三分鐘斷捨離,讓每天都早點下班

me
林彥成
2023-10-05 | 4 min.
文章目錄
  1. 1. Interface Segregation Principle
    1. 1.1. 定義介面
    2. 1.2. 通用處理函數

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

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

Interface Segregation Principle

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

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
// 拆開
// 初始版本只有基本角色屬性
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 定義一個函數,用於檢查玩家的健康狀態
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),介面隔離原則有助於確保程式碼的結構清晰,並使不同部分之間的依賴關係簡化,同時支持未來的擴充和修改。


喜歡這篇文章,請幫忙拍拍手喔 🤣


share