控制反轉 IoC 與設計模式高品質實戰指南 應用策略模式與依賴注入實現高度去耦合架構

me
林彥成
2023-10-01 | 6 min.
文章目錄
  1. 1. 什麼是控制反轉 (IoC)?
  2. 2. Guard Clause:提早返回優化可讀性
  3. 3. Railway Programming:流暢的錯誤處理
  4. 4. Strategy Pattern:行為封裝與解耦
  5. 5. Inversion of Control (IoC):架構的核心翻轉
    1. 5.1. Dependency Injection
  6. 6. 好萊塢法則 (Hollywood Principle)
  7. 7. FAQ:控制反轉常見問題
    1. 7.1. Q1:IoC 與 Dependency Injection (DI) 是同一件事嗎?
    2. 7.2. Q2:使用 IoC 會不會讓程式碼變得難以追蹤?
    3. 7.3. Q3:React 中除了 Props 傳遞,還有哪些 IoC 的實踐?

什麼是控制反轉 (IoC)?

控制反轉 (Inversion of Control, IoC) 是一種軟體架構設計原則,其核心在於將程式碼的執行流程控制權,從程式碼本身「反轉」給外部容器或框架。這就是著名的「好萊塢法則」:不要給我們打電話,我們會給你打電話。透過 IoC,元件不再自行負責依賴物件的建立與生命週期管理,而是透過 依賴注入 (Dependency Injection, DI)策略模式 (Strategy Pattern) 由外部提供所需資源。這種設計大幅降低了元件間的耦合度,提升了系統的可測試性與擴展性,是現代 Web 框架(如 Spring, Angular)與高品質 React 架構的基石。


當我們想要整理和分類物品的時候,會有許多判斷和想法,通常會將物品按照特定的步驟分類。在軟體開發中,我們也經常面臨類似的挑戰:如何處理不斷增加的判斷條件?

  1. 依照類型: 把不須冷藏食物放櫃子、需冷層或冷凍食物放在冰箱中
  2. 依照大小: 確定空間是否足夠大來容納一個物品
  3. 依照用途: 蔬菜放下層、冷凍食品放上層
  4. 依照期限: 期限長短由內至外排序

在程式裡面的體現就是 if-else 條件語句,根據不同的條件執行不同的程式碼塊。然而,當條件變得過於複雜時,我們需要應用 軟體設計模式 來進行 程式碼重構,避免邏輯失控。

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) {
// ...
}
}

在上面的例子中,我們就會發現判斷的條件越來越多,隨著角色的擴充邏輯就會漸漸失控。如果老闆希望針對成年使用者增加隱藏功能,程式會變得更加臃腫。

1
2
3
4
// 檢查玩家是否成年
function isPlayerAdult(player) {
return player.age >= 18;
}

小編在過去幾年的經驗當中,歸納出底下四種優化邏輯控制的方法,這也是 軟體架構設計 中的核心技巧:

  • Guard Clause (衛句)
  • Railway Programming (鐵路導向編程)
  • Strategy Pattern (策略模式)
  • Inversion of Control (控制反轉)

Guard Clause:提早返回優化可讀性

Guard Clause 是 JavaScript 程式碼中一種強大且簡單的技巧,目標是為了取代嵌套過深的 if-else 判斷語句,實現更乾淨的 程式碼重構

Guard Clause 是 JavaScript 程式碼中的一種常見技巧,目標是為了取代複雜的 if-else 判斷語句。

守衛的目的主要提前檢查並處理錯誤,只要提早發現錯誤就提早處理錯誤並提早跳出。

以剛剛的年齡判斷來說 Guard Clause 的範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function checkAge(age) {
// 使用 Guard Clause 檢查年齡是否小於 0
if (age < 0) {
console.log("年齡不能為負數");
return; // 提前返回,避免後續程式執行
}

// 使用 Guard Clause 檢查年齡是否小於 18
if (age < 18) {
console.log("你尚未成年");
return; // 提前返回,避免後續程式執行
}

// 如果年齡符合標準,執行後續操作
console.log("你是一個成年人");
// 這裡可以繼續執行其他操作
}

// 測試函數
checkAge(25); // 你是一個成年人
checkAge(12); // 你尚未成年
checkAge(-5); // 年齡不能為負數

在這個例子中,我們使用了兩個 Guard Clause 來檢查不合格的情況,即年齡是否小於 0 和是否小於 18,如果發現不符合成年人條件,函式會提前結束,並輸出相應的訊息。

Guard clauses 在 JavaScript 中的使用場景通常包括檢查函數的參數,處理邊界情況,確保數據有效性,並提前處理錯誤。

這有助於減少錯誤和不正確的操作,同時提高了程式碼的可靠性和可讀性。

Railway Programming:流暢的錯誤處理

Railway Programming 結合了 Functional Programming 技巧,將資料流比喻為鐵軌,根據處理成功 (Right) 或失敗 (Left) 決定火車的方向。這種模式能優化複雜的鏈式調用,讓邏輯層次分明。

Functional Programming 中的 Either 就是一個包著正確值或是錯誤的盒子,以下是一個簡單的 Railway Programming 使用 Either 來表示成功和失敗的情況,檢查年齡是否為成年,然後執行一個操作。

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
const Either = {
left: (value) => ({
isLeft: true,
value,
}),

right: (value) => ({
isLeft: false,
value,
}),
};

const isInvalidAge = (age) => isNaN(age) || age < 0;
const isChild = (age) => age < 18;

function checkAge(age) {
if (isInvalidAge(age)) {
return Either.left("年齡不能為負數");
}

if (isChild(age)) {
return Either.left("未成年");
}

return Either.right("成年");
}

function processAge(age) {
return checkAge(age).isLeft
? `錯誤:${checkAge(age).value}`
: `結果:${checkAge(age).value}`;
}

console.log(processAge(25)); // 結果:成年
console.log(processAge(15)); // 結果:未成年
console.log(processAge("abc")); // 錯誤:年齡無效

Strategy Pattern:行為封裝與解耦

策略模式用於在元件間共享行為和邏輯,目的是將可互換的行為封裝成獨立的策略物件,在運行時動態選擇適當的策略來執行特定的任務。

可以幫助實現元件的

  • 去耦合
  • 可重用性
  • 可擴展性

在 React 中,策略模式可以應用於元件的行為和邏輯。

  1. 策略元件: StrategyA 和 StrategyB,這兩個元件分別實現了不同的行為
    • 策略是 ‘A’ -> 渲染 StrategyA
    • 策略是 ‘B’ -> 渲染 StrategyB
  2. ContextComponent 是上下文元件,根據傳遞的策略選擇渲染相應的策略元件
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
// 策略元件
const StrategyA = ({ data }) => {
// 策略 A 的實現
return <div>Strategy A: {data}</div>;
};

const StrategyB = ({ data }) => {
// 策略 B 的實現
return <div>Strategy B: {data}</div>;
};

// 上下文元件
const ContextComponent = ({ strategy, data }) => {
// 根據選擇的策略渲染對應的元件
if (strategy === "A") {
return <StrategyA data={data} />;
} else if (strategy === "B") {
return <StrategyB data={data} />;
}
};

const App = () => {
const strategy = "A"; // 選擇策略 A 或 B
const data = "Hello, Strategy Pattern!";

return <ContextComponent strategy={strategy} data={data} />;
};

以剛剛年齡的例子來說

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 策略 A:確保年齡為正數
// 策略 B:確保年齡成年
const AgeVerificationStrategies = {
adult: (age) => age >= 18,
positive: (age) => age >= 0,
};

const ContextComponent = ({ strategy, age }) => {
const verifyAge = AgeVerificationStrategies[strategy];

if (verifyAge(age)) {
return <div>年齡驗證通過:{age} 歲</div>;
} else {
return <div>年齡驗證失敗:{age} 歲</div>;
}
};

const App = () => {
const strategy = "adult"; // 選擇策略 adult 或 positive
const age = 25;

return <ContextComponent strategy={strategy} age={age} />;
};

Inversion of Control (IoC):架構的核心翻轉

控制反轉是一個常見改變程式結構和元件相互互動方式的一種方式,提倡將控制權交給框架或容器,將控制權交給使用的人。

面板加上輸出端子會變成螢幕,加了上網功能搭配遙控器後就成為上網電視

遙控器就是控制反轉的一個概念,我們不直接操作上網電視而是透過遙控器,不管是實體遙控器或著是手機 APP 都可以達到一樣的目的,電視的操作流程不被預先定義,而是當使用者使用時才用遙控器去進行各種不同的操作流程。

底下舉兩個簡單的例子,透過把 normalize 的控制權放到 props 就能夠動態的去改動 input 文字大小寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
const Input = ({ normalize }) => {
const [value, setValue] = useState("");

return (
<input
value={text}
onChange={({ target }) => setValue(normalize(target.value))}
/>
);
};

<Input normalize={(text) => text.toUpperCase()} />;
<Input normalize={(text) => text.toLowerCase()} />;

把剛剛的例子加上年齡限制

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
const Input = ({ onInputChange, isAgeValid }) => {
const [value, setValue] = useState("");
const [isValid, setIsValid] = useState(true);

const handleInputChange = ({ target }) => {
const inputValue = target.value;
const normalizedValue = onInputChange(inputValue);
setValue(normalizedValue);

// 檢查年齡限制
const age = parseInt(normalizedValue, 10);
const isValidAge = isAgeValid(age);
setIsValid(isValidAge);
};

return (
<div>
<input
value={value}
onChange={handleInputChange}
style={{ borderColor: isValid ? "green" : "red" }}
/>
{!isValid && <p>年齡必須在 18 到 100 歲之間</p>}
</div>
);
};

const App = () => {
const toUpperCaseNormalize = (text) => text.toUpperCase();
const isAgeValid = (age) => age >= 18 && age <= 100;

return (
<div>
<Input onInputChange={toUpperCaseNormalize} isAgeValid={isAgeValid} />
</div>
);
};

Dependency Injection

實現 IoC 有多種方式,通常與依賴注入(DI)密切相關。

元件或物件都是從外部提供其依賴的功能或服務而不是在內部建立它們。

以剛剛那個例子來說

1
2
3
const isAgeValid = (age) => age >= 18 && age <= 100;

<Input onInputChange={toUpperCaseNormalize} isAgeValid={isAgeValid} />;

isAgeValid 就被注入到 Input 元件中,這樣即使驗證年齡的法規變了也可以輕易的改動,而不影響程式的核心的輸入邏輯。

好萊塢法則 (Hollywood Principle)

「不要給我們打電話,我們會給你打電話 (Don’t call us, we’ll call you)。」

這是 IoC 的哲學精髓。它提供了時間與使用上的極大彈性,讓高層模組定義流程,而將具體實作細節留給底層模組。這種設計思維是現代 軟體設計模式 的基石,能有效幫助開發者從繁瑣的條件判斷中解脫,打造優雅且易於維護的系統。掌握 IoC,就是掌握了通往高效開發的鑰匙。


FAQ:控制反轉常見問題

Q1:IoC 與 Dependency Injection (DI) 是同一件事嗎?

A:不是。 IoC (控制反轉) 是一個大原則、一種設計思想;而 DI (依賴注入) 則是實現該思想的最常見「技術手段」。簡單來說,IoC 是目的,DI 是手段。

Q2:使用 IoC 會不會讓程式碼變得難以追蹤?

A:初學者可能會覺得跳來跳去很麻煩,但這換來的是極高的「可測試性」。因為邏輯被拆分且可注入,您可以輕鬆地在測試中注入「Mock 物件」來模擬各種複雜情境,而無需修改核心程式碼。

Q3:React 中除了 Props 傳遞,還有哪些 IoC 的實踐?

A:Context API 是典型的 IoC 實踐。它將狀態管理的控制權從單一元件提升到 Provider 容器中;Render PropsHOC (Higher-Order Components) 也是透過反轉渲染邏輯的控制權來達成程式碼重用的技巧。


掌握了 IoC,您就能從繁瑣的 if-else 中解脫,讓程式碼從「命令式」轉向更優雅的「聲明式」。持續優化您的架構思維,讓開發變得更有預測性!


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