JavaScript Behavioral Pattern 開箱 JavaScript 設計模式 (3)

me
林彥成
2023-06-30 | 6 min.
文章目錄
  1. 1. Strategy Pattern (策略模式)
    1. 1.1. Inversion of Control (控制反轉)
  2. 2. Iterator Pattern (迭代器模式)
  3. 3. State Pattern
  4. 4. Command Pattern (命令模式)
  5. 5. Observer Pattern (觀察者模式)

這篇文章會介紹 JavaScript 的 Behavioral Patterns,行為模式是一種設計模式,它關注的是專注在物件之間的溝通。

行為模式組織物件之間的關係,提高可讀性和可維護性,下面是幾個常見的 JavaScript 行為模式:

  • Strategy Pattern (策略模式): 考前可以選擇不同的學習策略來應考,可以根據不同的情況選擇不同的解決方法。
  • Iterator Pattern (迭代器模式): 讀書時依序讀不同章節一樣,在程式中可以逐步訪問集合中的每個元素。
  • State Pattern (狀態模式): 根據天氣的不同,穿著不同的衣服,狀態模式根據不同的狀態改變自己的行為。
  • Command Pattern (命令模式): 可以把每個操作想像成一個命令,每個命令都有特定的任務或功能,並且可以被執行。
  • Observer Pattern (觀察者模式): 就像你訂閱你喜歡的 YouTuber 一樣,當有新的影片時會收到通知,建立訂閱和通知的機制。

Design Patterns 依照目的分成三群:

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} />;
};

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()} />;

Iterator Pattern (迭代器模式)

Iterator Pattern 目的是將遍歷集合的邏輯從集合本身分離出來,使得遍歷操作與集合的實現相互獨立,可以簡單地更換遍歷方式或使用不同的迭代器來遍歷不同類型的集合。

在程式中不會再出現傳統的 forwhile,提供了統一的方式來遍歷不同類型的集合提高可讀性和可維護性。

React 的迭代器模式範例:

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

// 遍歷器元件
const IteratorComponent = ({ iterable }) => {
const iterator = iterable[Symbol.iterator]();

return (
<div>
<h2>Iterator Component</h2>
<ul>
{Array.from(iterator).map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};

const App = () => {
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" },
];

return (
<div>
<h1>Iterator Pattern Example</h1>
<ListComponent items={items} />
<IteratorComponent iterable={items} />
</div>
);
};

State Pattern

當提到 React 的狀態模式 (State Pattern),通常指的是 React 的內建狀態管理機制,在 React 中每個元件都可以擁有自己的狀態,並且通過特定方式來管理和更新。

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

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

需要注意的是,React 的狀態模式適用於較小規模的應用和較簡單的元件間溝通,對於大型應用和較複雜的狀態管理需求,可以考慮使用 Redux 或是 zuszand 來搭配 React 使用。

接下來舉一個簡單的購物車應用,包含兩個元件:

  • ProductList: 顯示可用的商品列表,每個商品都有一個 “加入購物車” 的按鈕
    • addToCart: 是一個函數,用於將商品添加到購物車中
    • 當按下 “加入購物車” 的按鈕時,透過 handleAddToCart 函數,將商品傳遞給 addToCart 函數
  • ShoppingCart 顯示已選擇的商品列表以及總價格
    • cartItems: 該屬性是已選擇的商品列表,會根據 cartItems 計算總價格並顯示在頁面上
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 ProductList = ({ addToCart }) => {
const products = [
{ id: 1, name: "Product 1", price: 10 },
{ id: 2, name: "Product 2", price: 20 },
{ id: 3, name: "Product 3", price: 30 },
];

const handleAddToCart = (product) => {
addToCart(product);
};

return (
<div>
<h2>Product List</h2>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
<button onClick={() => handleAddToCart(product)}>
Add to Cart
</button>
</li>
))}
</ul>
</div>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ShoppingCart = ({ cartItems }) => {
const totalPrice = cartItems.reduce((total, item) => total + item.price, 0);

return (
<div>
<h2>Shopping Cart</h2>
<ul>
{cartItems.map((item) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
<p>Total Price: ${totalPrice}</p>
</div>
);
};

App 用於提升狀態,將狀態 cartItems 提升到 App 元件中,並通過 props 傳遞給子元件 ProductList 和 ShoppingCart,讓兩個元件共享購物車的狀態,實現了商品的添加和顯示功能。

  • cartItems: 被提升用於儲存已選擇的商品列表
    • ShoppingCart 元件可以使用從 App 元件傳遞的 cartItems 狀態
    • addToCart 函數將商品添加到購物車中並更新 cartItems 的狀態
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState } from "react";
import ProductList from "./ProductList";
import ShoppingCart from "./ShoppingCart";

const App = () => {
const [cartItems, setCartItems] = useState([]);

const addToCart = (product) => {
setCartItems([...cartItems, product]);
};

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

export default App;

Command Pattern (命令模式)

在 Command 模式中,可以將每個操作都視為一個命令 (Command),並將其表示為一個動作 (Action),舉個在 Redux 中的例子:

  1. Action: 在 Redux 中,動作是一個 Javascript 物件,它描述了要執行的操作類型和相關的資料,每個動作都應該有一個唯一的類型,例如 ADD_TODO 或 REMOVE_TODO。

  2. Action Creators: Action creator 是建立和封裝命令 (Command) 的函數,它接收必要的參數,並返回一個包含類型和資料的動作 (Action) 物件。

    1
    2
    3
    function doAddToDoItem(text) {
    return { type: "ADD_TODO", payload: text };
    }
  3. Redux Thunk 或 Redux Saga: 屬於 middleware 讓我們可以用非同步的方式處理並發出適當的動作 (Action)。

總結來說,在 Redux 中,我們可以將每個操作視為一個命令 (Command),並使用 Action Creators 和 middleware 來建立和執行這些命令。

Observer Pattern (觀察者模式)

Observer Pattern 的目的是建立一對多的依賴關係,當被觀察者 (Subject) 的狀態發生變化時,所有觀察者 (Observers) 都會被通知並進行相應的更新。

舉一個 Redux 和 React Redux 的例子,React Redux 提供了一個 connect 函數,這個函數將 React 元件與 Redux Store 的狀態連接起來,並將狀態變化傳遞給相應的元件,當 Redux 的狀態發生變化時,被連接的 React 元件將收到通知並進行更新,這樣可以實現元件之間的狀態共享和溝通,達到類似觀察者模式的效果。

舉個例子,底下程式定義了 counter 的 slice,並設置了初始狀態為 0,同時定義了 increment 和 decrement 兩個 reducer 函數,它們分別處理增加和減少計數器的邏輯。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
name: "counter",
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
},
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

在 React 元件中使用 useDispatch 和 useSelector hook 來操作 Redux store:

  • useSelector: 選擇並訪問 Redux store 中的 counter 狀態
  • useDispatch: 取得 dispatch 函數,可以用來調度 Redux store 中的動作

這樣,使用 createSlice 和 React Redux 配合使用,就能在 React 元件中使用觀察者模式的效果,實現狀態管理和元件間的溝通。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "./counterSlice";

const CounterDisplay = () => {
const counter = useSelector((state) => state.counter);
return <div>Counter: {counter}</div>;
};

const CounterControls = () => {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
};

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


share