JS 創建型設計模式實戰指南 Singleton 與 Factory 於 React 實例化過程之優化

me
林彥成
2023-02-25 | 7 min.
文章目錄
  1. 1. 什麼是創建型設計模式?
  2. 2. Class Design Pattern
    1. 2.1. Constructor Pattern
  3. 3. Singleton Pattern
  4. 4. Factory Pattern
  5. 5. Abstract Factory Pattern
  6. 6. FAQ:JavaScript 創建型設計模式常見問題
    1. 6.1. Q1:單例模式 (Singleton) 在前端開發中有副作用嗎?
    2. 6.2. Q2:什麼時候該用 Factory 模式而不是直接用 new?
    3. 6.3. Q3:Abstract Factory 與簡單 Factory 的區別在哪?

什麼是創建型設計模式?

創建型設計模式 (Creational Design Patterns) 是指在軟體工程中,專注於「如何建立物件」的一套設計方案。這類模式的核心目標是將物件的建立過程與其使用邏輯分離,從而隱藏物件建立的複雜細節,並在不影響系統靈活性的情況下,提供更優雅、更彈性的實例化方式。常見的創建型模式包括 Singleton(確保唯一實例)、Factory(封裝建立邏輯)以及 Abstract Factory(建立一系列相關物件家族),是提升 JavaScript 程式碼品質與可維護性的關鍵基礎。


在 JavaScript 程式設計中,如何有效地建立和管理物件是提升程式碼品質與可維護性的關鍵。JavaScript 設計模式提供了一系列經過驗證的解決方案,而創建型設計模式 (Creational Design Patterns) 則是專注於物件的建立機制,幫助我們在不犧牲系統靈活性的情況下,實現更優雅、更彈性的物件實例化過程。

本篇文章將深入介紹幾種核心的創建型設計模式,透過實際的 JavaScript 範例,解析它們在現代前端開發中的應用場景與優勢:

  • Class Design Pattern (類別設計模式):基於 ES6 Class 的物件創建方式。
  • Constructor Pattern (建構子模式):利用建構函式創建新物件的傳統方法。
  • Singleton Pattern (單例模式):確保一個類別只有一個實例,並提供一個全域訪問點。
  • Factory Pattern (工廠模式):定義一個用於創建物件的介面,但讓子類別決定實例化哪一個類別。
  • Abstract Factory Pattern (抽象工廠模式):提供一個介面,用於創建一系列相關或相互依賴物件的家族,而無需指定它們的具體類別。

理解這些創建型設計模式不僅能讓你更好地控制物件的生命週期,也能在面對複雜的物件建立邏輯時,提供清晰且可擴展的解決方案。

Design Patterns 依照目的分成三群:

Class Design Pattern

這個其實算常見,基於 prototype 的基礎在 ECMAScript 6 被實作,讓我們可以用模擬物件導向方式去建立物件。

Class 是一個定義檔,去定義出 "未來產生的" 物件應該要長什麼樣子以及該怎麼被使用。

在使用 Class 時必須使用 new 這個關鍵字用 Class 去產生出新的 Instance 作為物件來被使用。

在 JavaScript 中,ES6 引入的 class 語法糖讓物件導向的程式設計變得更加直觀。Class Design Pattern 強調了透過類別來定義物件的藍圖,包括其屬性(properties)和方法(methods)。

這種方式在構建可重用元件、定義資料結構以及實現繼承時尤為方便。它不僅提高了程式碼的可讀性和組織性,也為實現如 Factory PatternSingleton Pattern 等更複雜的創建型設計模式提供了堅實的基礎。

1
2
3
4
5
6
7
8
9
10
11
class Motorcycle {
constructor(license, volume) {
this.license = license;
this.volume = volume;
}
}

const motor125 = new Motorcycle("white", 125);
const motor50 = new Motorcycle("white", 50);
console.log(motor125);
console.log(motor50);

執行程式碼的結果

Motorcycle {license: ‘white’, volume: 125}
Motorcycle {license: ‘white’, volume: 50}

Constructor Pattern

以 Class Design Pattern 延伸出的就是 Constructor Pattern,讓我們可以沿用父類別 constructor 已經定義過的變數和沿用該變數該有的行為。

Constructor Pattern (建構子模式) 是 JavaScript 中一種基礎的物件創建方式,它允許我們透過一個建構函式來創建具有相同屬性和方法的物件實例。當我們需要創建多個類似物件時,使用建構子模式可以提高程式碼的重用性並確保物件的一致性。

在繼承的場景中,子類別的建構子可以透過 super() 呼叫父類別的建構子,有效管理屬性的初始化,這對於實現複雜的JavaScript 設計模式,特別是涉及物件階層的創建型設計模式,提供了清晰的結構。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Motorcycle {
constructor(license, volume) {
this.license = license;
this.volume = volume;
}
}

class SymMotor extends Motorcycle {
constructor(license, volume) {
super(license, volume);
this.brand = "sym";
}
}

const symMotorGreenLicense = new SymMotor("green", 50);
const symMotorWhiteLicense = new SymMotor("white", 125);
console.log(symMotorGreenLicense);
console.log(symMotorWhiteLicense);

執行程式碼的結果

SymMotor {license: ‘green’, volume: 50, brand: ‘sym’}
SymMotor {license: ‘white’, volume: 125, brand: ‘sym’}

React 初期的 class component 就是運用這個來實作出元件,super 會參照父類別的 constructor,並且要在呼叫過後才可以叫用 this,所以我們可以去使用已經在父類別裡面已經被定義過的 props,以 Motorcycle 當例子就是 license 和 volume 都已經被定義過且被使用。

如果在寫 Super 前就先使用 this 去定義 function 若 function 有使用到 props 的內容就會造成未定義的問題,所以在 constructor 中使用 this,JavaScript 強制必須先呼叫 super。

那為什麼要寫一個 super(props); 當參數,原因就在 React 原始碼裡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Base class helpers for the updating state of a component.
*/
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Class-based
class Input extends React.Component {
constructor() {
this.handleInputInit(); // 不被允許
super(props);
this.state = { input: "" };

this.handleInput = this.handleInput.bind(this);
}

handleInputInit() {
// 以 Motorcycle 來說這裡就會看不到 license 和 volume
}

handleInput(e) {
this.setState({ input: e.target.value });
}

render() {
<input onChange={handleInput} value={this.state.input} />;
}
}

Singleton Pattern

單體模式算是小編接觸的第一個設計模式,目標很清楚就是要持久化一個物件,當未來每次要產生物件時其實都是使用同一個 Instance 的物件內容。

不過較常使用在後端,讓用於資料庫連線的物件只會有一個。

Singleton Pattern (單例模式)創建型設計模式中最廣為人知且實用的一種,它確保一個類別只有一個實例,並提供一個全域訪問點。在 JavaScript 開發中,單例模式常用於管理應用程式的共享資源,例如配置管理器、狀態管理器(如 Redux store)、全域事件總線或資料庫連接池(在 Node.js 後端環境中)。

透過限制類的實例數量為一個,Singleton Pattern 有助於節省系統資源、避免命名衝突,並提供對單一共享點的統一控制。然而,過度使用單例模式也可能導致程式碼緊密耦合,增加測試難度,因此需謹慎權衡其優勢與潛在缺點。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let instance = null;

class Motorcycle {
constructor(license, volume) {
if (instance) return instance;
if (!instance) {
this.license = license;
this.volume = volume;
instance = this;
}
}
}

const motor125 = new Motorcycle("white", 125);
const motor50 = new Motorcycle("white", 50);
console.log(motor125);
console.log(motor50);

執行程式的結果可以發現都是相同物件的內容,新的 new 其實並沒有產生新的物件出來。

Motorcycle {license: ‘white’, volume: 125}
Motorcycle {license: ‘white’, volume: 125}

Factory Pattern

工廠模式顧名思義就是去定義出類似工廠概念的 class,工廠是按照規格把東西做出來。

所以 Factory Pattern 就會是專注定義規格,並且提供 function 讓使用者可以將物件製造出來。

Factory Pattern (工廠模式) 是另一種核心的創建型設計模式,它提供了一個介面來創建物件,但允許子類別決定要實例化哪個類別。在 JavaScript 中,這意謂著你可以將物件創建的邏輯集中管理,避免在客戶端程式碼中直接使用 new 關鍵字來實例化物件。

這種模式的優點在於解耦了客戶端程式碼與具體產品類別之間的依賴,使得新增或修改產品類別時,只需修改工廠邏輯而無需改動所有使用該產品的地方。Factory Pattern 廣泛應用於需要根據不同條件返回不同物件實例的場景,例如遊戲中的角色創建、不同類型日誌記錄器的選擇等,是實現高度彈性與可擴展性的JavaScript 設計模式

底下的例子我們就來定義機車的規格

  • 50cc 綠牌車
  • 125cc 白牌車
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
// 1. 定義基礎 Class 一台機車包含牌照及 CC 數
class Motorcycle {
constructor(license, volume) {
this.license = license;
this.volume = volume;
}
}

// 2. 定義 Sym 工廠並且定義 createMotor function
class MotorFactory {
createMotor({ volume }) {
if (volume <= 50) {
return new Motorcycle("green", volume);
}

if (volume < 150) {
return new Motorcycle("white", volume);
}
}
}

// 建立工廠物件
const motorFactory = new MotorFactory();

// 透過工廠物件生產機車
const motorGreenLicense = motorFactory.createMotor({ volume: 50 });
const motorWhiteLicense = motorFactory.createMotor({ volume: 125 });

console.log(motorGreenLicense);
console.log(motorWhiteLicense);

執行程式碼的結果

Motorcycle {license: ‘green’, volume: 50}
Motorcycle {license: ‘white’, volume: 125}

如果要寫出一個 react 版本的

1
2
3
4
5
6
7
8
9
function factory(params) {
const condition = "some condition";

if (condition) {
return <ProductA params={params} />;
} else {
return <ProductB params={params} />;
}
}

Abstract Factory Pattern

Abstract Factory 只是 Factory Pattern 延伸的概念,以現實狀況來說就是一個代工廠。

小編現在剛好就在代工廠上班,代工廠做的事情是在某個規格的基礎上,依照各品牌去做相關的客製化做代工,舉例來說常見的鞋子、筆電等等,其實都是由代工廠為各個品牌進行製造的。

以廣達當作 Abstract Factory 來看,實際的代工出來的就會有 HP、Toshiba、Sony、Lenovo 等等品牌筆電。

Abstract Factory Pattern (抽象工廠模式)工廠模式的一個更進階的應用,它提供一個介面,用於創建一系列相關或相互依賴物件的家族,而無需指定它們的具體類別。這種創建型設計模式的優勢在於,它能夠確保所創建物件家族之間的兼容性,同時讓客戶端程式碼與具體工廠和產品之間保持高度解耦。

在 JavaScript 開發中,當你需要支援多種平台(如 Web、Mobile)或多個主題(如亮色模式、暗色模式),且每個平台或主題都有一套相關聯的 UI 元件或服務時,Abstract Factory Pattern 便能提供一個優雅的解決方案。它提升了系統的彈性,使其更容易擴展以適應新的產品系列或環境,是構建複雜且可配置系統的關鍵JavaScript 設計模式

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
// 1. 定義基礎 Class 一台機車包含牌照及 CC 數
class Motorcycle {
constructor(license, volume) {
this.license = license;
this.volume = volume;
}
}

// 2. 定義各品牌
class SymMotor extends Motorcycle {
constructor(license, volume) {
super(license, volume);
this.brand = "sym";
}
}

class KymcoMotor extends Motorcycle {
constructor(license, volume) {
super(license, volume);
this.brand = "Kymco";
}
}

// 3. 定義品牌工廠
class SymMotorFactory {
createMotor(volume) {
if (volume <= 50) {
return new SymMotor("green", volume);
}

if (volume < 150) {
return new SymMotor("white", volume);
}
}
}

class KymcoMotorFactory {
createMotor(volume) {
if (volume <= 50) {
return new KymcoMotor("green", volume);
}

if (volume < 150) {
return new KymcoMotor("white", volume);
}
}
}

// 建立工廠物件
const symMotorFactory = new SymMotorFactory();
const kymcoMotorFactory = new KymcoMotorFactory();

// 建立抽象工廠 (代工廠)
const motorFactory = ({ brand, volume }) => {
if (brand === "sym") {
return symMotorFactory.createMotor(volume);
}
if (brand === "kymco") {
return kymcoMotorFactory.createMotor(volume);
}
};

// 讓代工廠進行製造
const symMotorGreenLicense = motorFactory({ brand: "sym", volume: 50 });
const kymcoMotorGreenLicense = motorFactory({ brand: "kymco", volume: 50 });
const symMotorWhiteLicense = motorFactory({ brand: "sym", volume: 125 });

console.log(symMotorGreenLicense);
console.log(symMotorWhiteLicense);
console.log(kymcoMotorGreenLicense);

程式執行結果

SymMotor {license: ‘green’, volume: 50, brand: ‘sym’}
SymMotor {license: ‘white’, volume: 125, brand: ‘sym’}
KymcoMotor {license: ‘green’, volume: 50, brand: ‘Kymco’}


FAQ:JavaScript 創建型設計模式常見問題

Q1:單例模式 (Singleton) 在前端開發中有副作用嗎?

A:有的。雖然單例模式能節省記憶體,但在單元測試中,全域共享的狀態可能導致測試案例互相干擾。此外,它也會讓元件之間的耦合度變高,增加維護難度。

Q2:什麼時候該用 Factory 模式而不是直接用 new

A:當物件建立邏輯變得複雜(例如需要根據輸入決定實例化哪種類別),或者當您想要將「建立物件」與「使用物件」這兩件事完全解耦時,Factory 模式就是最佳選擇。

Q3:Abstract Factory 與簡單 Factory 的區別在哪?

A:簡單 Factory 通常只生產單一種類型的物件(如:只產機車);而 Abstract Factory 生產的是一個「系列」或「家族」的物件(如:品牌代工廠,同時處理不同品牌的規格客製化)。


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