JS 行為型設計模式優化 Strategy 與 Observer React 實踐

me
林彥成
2023-06-30 | 5 min.
文章目錄
  1. 1. 為什麼需要行為型設計模式?
  2. 2. 1. Strategy Pattern (策略模式)
    1. 2.1. Strategy Pattern (策略模式) 核心價值
    2. 2.2. React 實戰範例
    3. 2.3. Inversion of Control (控制反轉)
  3. 3. 2. Iterator Pattern (迭代器模式)
    1. 3.1. Iterator Pattern (迭代器模式)核心價值
    2. 3.2. React 範例
  4. 4. 3. State Pattern (狀態模式)
    1. 4.1. State Pattern (狀態模式) 核心價值
    2. 4.2. 狀態提升 (State Lifting) 範例
  5. 5. 4. Command Pattern (命令模式)
    1. 5.1. Command Pattern (命令模式) 核心價值
  6. 6. 5. Observer Pattern (觀察者模式)
    1. 6.1. 核心價值
    2. 6.2. 事件監聽範例
  7. 7. FAQ:JavaScript 行為型設計模式常見問題
    1. 7.1. Q1:策略模式與狀態模式有什麼區別?
    2. 7.2. Q2:為什麼在 React 中需要了解這些模式?
    3. 7.3. Q3:行為型模式會讓程式碼變複雜嗎?

在 JavaScript 開發中,行為型設計模式 (Behavioral Design Patterns) 是指專注於物件之間如何進行溝通、職責分配以及協作完成任務的一套設計方案。與創建型或結構型模式不同,行為型模式更關心的是「演算法」與「物件間的交互關係」。掌握這些模式能顯著提升程式碼的靈活性、重用性與可維護性,是進階開發者的必修課。

為什麼需要行為型設計模式?

當應用程式變得複雜時,物件之間的依賴關係往往會變得難以管理。行為型設計模式的主要優勢包括:

  1. 降低耦合度:讓物件不需要知道彼此的內部細節即可進行協作。
  2. 提升靈活性:可以在執行時動態改變物件的行為(如策略模式或狀態模式)。
  3. 結構清晰:將複雜的遍歷、通知或請求轉發邏輯從核心業務中分離出來。

本文將解析 5 種核心模式:StrategyIteratorStateCommandObserver,並結合 React 實戰範例。

  • Strategy Pattern (策略模式):定義一系列演算法,將它們封裝起來,使它們可以互相替換,且不影響客戶端。
  • Iterator Pattern (迭代器模式):提供一種方法順序訪問聚合物件中的各個元素,而又不暴露該物件的內部表示。
  • State Pattern (狀態模式):允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它的類別。
  • Command Pattern (命令模式):將一個請求包裝成一個物件,從而允許你使用不同的請求、佇列或日誌來參數化客戶端。
  • Observer Pattern (觀察者模式):定義物件間一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知並自動更新。

掌握這些行為型設計模式將幫助你更好地組織程式碼邏輯,處理複雜的互動行為,從而開發出更具彈性和可維護性的 JavaScript 應用。


Design Patterns 依照目的分成三群:

1. Strategy Pattern (策略模式)

策略模式定義了一系列演算法,將它們封裝起來,使它們可以互相替換,而不影響客戶端。

Strategy Pattern (策略模式) 核心價值

透過將不同的行為封裝成獨立的策略物件,程式碼變得更加模組化。這在處理表單驗證、不同支付方式或用戶角色權限時非常實用。

React 實戰範例

在 React 中,我們可以根據 props 傳入的策略動態決定渲染邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
// 策略元件
const StrategyA = ({ data }) => <div>策略 A 實作: {data}</div>;
const StrategyB = ({ data }) => <div>策略 B 實作: {data}</div>;

// 上下文元件 (Context)
const ContextComponent = ({ strategy, data }) => {
const strategies = {
A: <StrategyA data={data} />,
B: <StrategyB data={data} />,
};
return strategies[strategy] || <div>請選擇策略</div>;
};

Inversion of Control (控制反轉)

控制反轉是一種軟體設計原則,它提倡將控制權交給框架或容器,由它們來管理對象的建立和依賴關係。這種模式可以幫助實現程式碼的鬆耦合和可測試性。

原本的元件可以想像是一台車子,要控制車子就必須坐進去車子中,控制反轉是將車子設計成遙控車的概念,透過介面來進行控制。

底下舉兩個簡單的例子,透過把 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()} />;

2. Iterator Pattern (迭代器模式)

迭代器模式提供一種方法來順序訪問聚合物件中的各個元素,而無需暴露該物件的內部表示。

Iterator Pattern (迭代器模式)核心價值

將遍歷邏輯與資料結構解耦。在現代 JavaScript 中,Symbol.iterator 讓任何物件都能成為可迭代對象,支援 for...of 語圈。

在 JavaScript 中,原生的 for...of 迴圈以及 Array.prototype.forEachMapSet 等都內建了迭代器概念。此模式的核心價值在於將遍歷邏輯與資料結構解耦,使得你可以對任何可迭代的資料結構使用統一的遍歷介面。

React 範例

React 的迭代器模式範例:

  1. ListComponent 接受 items 屬性後使用 map 函數來遍歷 items。
  2. IteratorComponent 透過 iterable[Symbol.iterator]() 方法取到物件迭代器,將迭代器轉換為陣列並使用 map 函數遍歷每個元素。
1
2
3
4
5
6
7
8
9
const IteratorComponent = ({ items }) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};

3. State Pattern (狀態模式)

狀態模式允許一個物件在其內部狀態改變時改變它的行為。在 React 中,這與元件的生命週期與 useState 息息相關。

React 透過狀態和 setState 方法,提供便捷的方式來管理和更新狀態:

  • Component State (元件狀態): 狀態是純 JavaScript 物件在元件內部進行初始化和更新,用於儲存元件所需數據
  • setState (修改狀態的方法): setState 接受新的狀態物件或一個 callback function 來計算新狀態並更新元件,這個過程是非同步操作,會將更新一次性進行批次處理以提高性能
  • Immutability (不可變性): 在更新狀態時,應該建立新的狀態物件,可以幫助 React 更好地處理狀態變化,進行有效的重新渲染,確保狀態的可預測性和性能優化
  • State Lifting (狀態提升): 當多個元件需要共享狀態或進行狀態同步時,可以將狀態提升到共同父元件中,使狀態在元件樹中傳遞,從而實現元件間的數據的共用和溝通,使得元件間的狀態共享和溝通變得更加靈活和可控

State Pattern (狀態模式) 核心價值

避免大量的 if-elseswitch。例如,一個訂單元件在「待處理」、「處理中」、「已完成」狀態下會有完全不同的按鈕與提示訊息。

狀態提升 (State Lifting) 範例

將狀態提升到父元件,讓多個子元件共用狀態:

1
2
3
4
5
6
7
8
9
10
11
const App = () => {
const [cartItems, setCartItems] = useState([]);
const addToCart = (product) => setCartItems([...cartItems, product]);

return (
<div>
<ProductList addToCart={addToCart} />
<ShoppingCart cartItems={cartItems} />
</div>
);
};

4. Command Pattern (命令模式)

命令模式將一個請求包裝成一個物件,從而允許你使用不同的請求、佇列或日誌來參數化客戶端。

Command Pattern (命令模式) 核心價值

最典型的例子是 Reduxaction。我們發出一個命令(Action Object),由 Reducer(Receiver)來處理,這實現了「意圖」與「實作」的完全解耦。

1
2
3
4
5
// Action Creator (建立命令)
const doAddToDoItem = (text) => ({ type: "ADD_TODO", payload: text });

// Dispatching the command
dispatch(doAddToDoItem("學習設計模式"));

5. Observer Pattern (觀察者模式)

觀察者模式定義物件間一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知並自動更新。

核心價值

它是響應式程式設計(Reactive Programming)的基石。在 React-Redux 中,useSelector 就是一種觀察者,當 Store 狀態改變時,它會通知元件重新渲染。

事件監聽範例

1
2
3
4
// DOM 原生觀察者模式
window.addEventListener('resize', () => {
console.log('視窗大小已改變');
});

FAQ:JavaScript 行為型設計模式常見問題

Q1:策略模式與狀態模式有什麼區別?

A:策略模式通常是由客戶端主動選擇演算法,且各策略間通常是獨立的;而狀態模式則是在物件內部根據狀態變化自動切換行為,且各狀態間通常存在切換邏輯。

Q2:為什麼在 React 中需要了解這些模式?

A:React 的 Hook(如 useReducer)與 Context API 的設計深受這些模式影響。了解命令模式有助於精進 Redux 開發,了解觀察者模式能幫助你理解為什麼狀態更新會觸發渲染,從而優化效能。

Q3:行為型模式會讓程式碼變複雜嗎?

A:在簡單的應用中可能會顯得過度設計(Over-engineering),但在中大型專案中,它們能有效防止程式碼變成難以維護的「義大利麵條(Spaghetti code)」,長期來看是降低複雜度的。


透過掌握這 5 大行為型設計模式,您將能更有系統地處理物件間的複雜互動。不論是透過 Strategy 優化邏輯切換,還是運用 Observer 建立響應式系統,這些技巧都將讓您的 JavaScript 程式碼更上一層樓!


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