設計模式篇:在前端,我們如何“重構”觀察者、策略和裝飾器模式
引子:代碼里“似曾相識”的場景
作為開發者,我們總會遇到一些“似曾相識”的場景:
- “當這個數據變化時,我需要通知其他好幾個地方都更新一下。”
- “這里有一大堆
if...else
,根據不同的條件執行不同的邏輯,丑陋又難以擴展。” - “我需要給好幾個函數都增加一個相同的功能,比如記錄日志或檢查權限,但我不想去修改這些函數本身。”
這些場景,就像是編程世界里的“常見病”。而設計模式(Design Patterns),就是由前人總結出的、針對這些“常見病”的、經過千錘百煉的“經典藥方”。
然而,很多前端開發者一提到設計模式,可能會覺得它很“后端”、很“學院派”,充滿了復雜的UML圖和抽象的Java/C++示例,與我們日常用JavaScript/TypeScript構建的動態、響應式的世界格格不入。
這是一個巨大的誤解。
設計模式并非僵化的代碼模板,它是一種思想,一種解決特定問題的思路和詞匯。事實上,那些經典的GoF(《設計模式:可復用面向對象軟件的基礎》一書的四位作者)設計模式,早已化作“DNA”,深深地融入了現代前端框架和最佳實踐的血液里。只是它們換了一副更符合函數式、組件化編程思想的“面孔”。
今天,我們不當“考古學家”,去研究那些原始的、基于類的設計模式定義。我們將當一名“翻譯家”和“重構師”,帶著現代前端的視角,去重新發現和“重構”我們身邊最常見、最實用的三個設計模式:觀察者模式、裝飾器模式和策略模式。
你將看到,這些經典思想是如何在我們之前的代碼中“靈魂附體”的,以及我們如何能有意識地運用它們,寫出更優雅、更靈活、更具可擴展性的代碼。
第一幕:觀察者模式 - “你變了,我會知道”
模式定義:觀察者模式(Observer Pattern)定義了一種一對多的依賴關系,讓多個觀察者對象(Observer)同時監聽某一個主題對象(Subject)。當主題對象的狀態發生變化時,它會通知所有觀察者,使它們能夠自動更新自己。
這聽起來是不是無比熟悉?沒錯,它就是我們這個系列中反復出現的核心思想:響應式和數據驅動的基石。
場景重現:我們的“發布/訂閱”和“狀態機”
-
發布/訂閱模式 (EventBus)
在我們的第十章中,我們構建了一個類型安全的事件總線。- 主題(Subject):
EventBus
實例本身。 - 觀察者(Observer): 通過
bus.on('eventName', callback)
注冊的每一個callback
函數。 - 通知(Notify): 當調用
bus.emit('eventName', payload)
時,EventBus
遍歷并執行所有監聽'eventName'
的callback
。
- 主題(Subject):
-
Redux-like狀態機 (
createStore
)
在我們的第五章中,我們實現了一個createStore
函數。- 主題(Subject):
store
實例。 - 觀察者(Observer): 通過
store.subscribe(listener)
注冊的每一個listener
函數。 - 通知(Notify): 在
store.dispatch(action)
導致state
更新后,store
會遍歷并執行所有的listener
。
- 主題(Subject):
觀察者模式的核心,是解耦。主題對象(如store
)不關心誰在監聽它,也不關心觀察者們(如UI組件)收到通知后會做什么。它只負責在自己狀態變化時,吼一嗓子:“我變了!”。而觀察者們則可以獨立地決定如何響應這個變化。
這種解耦,是構建大型、可維護應用的基礎。它讓我們的數據層和視圖層可以獨立演進,而不會互相“糾纏”。
代碼“翻譯”
我們已經實現了它,現在我們用“模式”的語言來為它添加注釋,加深理解。
// createStore.ts
import { Action, Reducer, Store } from './types';export function createStore<S, A extends Action>(reducer: Reducer<S, A>,initialState: S
): Store<S, A> {// state: 這就是我們的“主題對象”的核心狀態let currentState: S = initialState;// listeners: 這就是“觀察者列表”const listeners: Array<() => void> = [];function getState(): S {return currentState;}function dispatch(action: A): void {currentState = reducer(currentState, action);// Notify: 當狀態變化后,通知所有觀察者listeners.forEach(listener => listener());}// subscribe: 這就是“注冊觀察者”的方法function subscribe(listener: () => void): () => void {listeners.push(listener);// 返回一個“取消注冊”的函數return function unsubscribe() {const index = listeners.indexOf(listener);listeners.splice(index, 1);};}return { getState, dispatch, subscribe };
}
第二幕:裝飾器模式 - “給你加個Buff,但不改變你”
模式定義:裝飾器模式(Decorator Pattern)允許向一個現有的對象動態地添加新的功能,同時又不改變其結構。它是一種對繼承具有很大靈活性的替代方案。
簡單來說,就是在不修改原函數代碼的情況下,為它包裹一層或多層“裝飾”,來增強其功能。
在傳統的面向對象語言中,這通常通過創建一個繼承自原類的“裝飾器類”來實現,非常繁瑣。但在函數式編程占主導的JavaScript世界里,我們有更優雅的實現方式:高階函數(Higher-Order Functions, HOF)。
一個接收函數作為參數,并返回一個新函數(增強版)的函數,就是一個高階函數,也是一個天然的“裝飾器”。
場景重現與代碼“翻譯”
假設我們有一個核心的數據獲取函數,我們想在不修改它本身的情況下,為它增加“日志記錄”和“性能監控”的功能。
dataFetcher.ts
(原始函數)
// 這是一個“純粹”的函數,只關心核心邏輯
async function fetchImportantData(id: string): Promise<{ data: string }> {console.log(`[Core] Fetching data for id: ${id}`);// 模擬網絡請求await new Promise(resolve => setTimeout(resolve, 500));return { data: `Some important data for ${id}` };
}
decorators.ts
(我們的高階函數裝飾器)
// 1. 日志裝飾器
function withLogging<T extends (...args: any[]) => any>(fn: T): T {const fnName = fn.name || 'anonymous';return function(...args: Parameters<T>): ReturnType<T> {console.log(`[Log] Entering function '${fnName}' with arguments:`, args);return fn(...args);} as T;
}// 2. 性能監控裝飾器
function withTiming<T extends (...args: any[]) => any>(fn: T): T {const fnName = fn.name || 'anonymous';return async function(...args: Parameters<T>): Promise<ReturnType<T>> {console.time(`[Perf] Function '${fnName}'`);try {return await fn(...args);} finally {console.timeEnd(`[Perf] Function '${fnName}'`);}} as T;
}
Parameters<T>
和ReturnType<T>
是TypeScript內置的工具類型,能從函數類型T
中分別提取出其參數類型和返回值類型,保證了裝飾器的類型安全。
使用裝飾器
// main.ts
import { fetchImportantData } from './dataFetcher';
import { withLogging, withTiming } from './decorators';// 像套娃一樣,一層一層地包裹(裝飾)
const decoratedFetch = withLogging(withTiming(fetchImportantData));// 調用被裝飾后的函數
decoratedFetch("user-123");/*預期輸出:[Log] Entering function 'withTiming' with arguments: [ 'user-123' ][Perf] Function 'fetchImportantData': start[Core] Fetching data for id: user-123[Perf] Function 'fetchImportantData': end 502.13ms
*/
看,我們沒有修改一行fetchImportantData
的代碼,就成功地為它增加了日志和計時功能。我們可以像搭積木一樣,自由地組合這些裝飾器,應用到任何需要的函數上。
在React的世界里,高階組件(Higher-Order Components, HOC),比如connect
from Redux或withRouter
from React Router,就是完全相同的思想,只不過它們裝飾的是“組件”,而非普通函數。
第三幕:策略模式 - “條條大路通羅馬,你想走哪條?”
模式定義:策略模式(Strategy Pattern)定義了一系列的算法,并將每一個算法封裝起來,使它們可以互相替換。策略模式讓算法的變化,獨立于使用算法的客戶。
換句話說,當實現一個目標的“路徑”或“策略”有多種時,不要用一大堆if...else if...else
把所有路徑都寫死在一個地方。而是把每一條“路徑”,都封裝成一個獨立的對象或函數,讓調用者可以根據需要,自由地選擇和切換“路徑”。
場景重演與代碼“翻譯”
假設我們的應用需要實現一個表單校驗功能。對于一個輸入框,可能有多種校驗規則:不能為空、必須是Email格式、必須達到最小長度等等。
反模式 (Ugly if...else
):
function validate(value: string, rules: string[]): boolean {for (const rule of rules) {if (rule === 'isNotEmpty') {if (value === '') return false;} else if (rule === 'isEmail') {if (!/^\S+@\S+\.\S+$/.test(value)) return false;} else if (rule.startsWith('minLength:')) {const min = parseInt(rule.split(':')[1]);if (value.length < min) return false;}}return true;
}
這段代碼的壞處顯而易見:每增加一種新的校驗規則,我們都必須修改這個函數,違反了“開閉原則”(對擴展開放,對修改關閉)。
策略模式重構
我們將每一種校驗規則,都封裝成一個獨立的“策略”對象。
validationStrategies.ts
// 定義策略的統一接口
interface ValidationStrategy {validate(value: string): boolean;message: string;
}// 策略對象集合
export const strategies: Record<string, ValidationStrategy> = {isNotEmpty: {validate: (value: string) => value.trim() !== '',message: 'Value cannot be empty.',},isEmail: {validate: (value: string) => /^\S+@\S+\.\S+$/.test(value),message: 'Value must be a valid email address.',},minLength: (min: number): ValidationStrategy => ({validate: (value: string) => value.length >= min,message: `Value must be at least ${min} characters long.`,}),
};
注意,minLength
我們實現為一個返回策略對象的函數(工廠模式),這讓它可以接收參數。
Validator.ts
(使用策略的客戶)
import { strategies, ValidationStrategy } from './validationStrategies';class Validator {private rules: ValidationStrategy[] = [];public add(ruleName: string, ...args: any[]): void {let strategy: ValidationStrategy;if (ruleName === 'minLength' && typeof strategies.minLength === 'function') {strategy = (strategies.minLength as Function)(...args);} else {strategy = strategies[ruleName];}if (strategy) {this.rules.push(strategy);}}public validate(value: string): string[] {const errors: string[] = [];for (const rule of this.rules) {if (!rule.validate(value)) {errors.push(rule.message);}}return errors;}
}
使用
// main.ts
const validator = new Validator();
validator.add('isNotEmpty');
validator.add('isEmail');
validator.add('minLength', 8);const errors = validator.validate('test@test.com');
console.log(errors); // [] (no errors)const errors2 = validator.validate(' test ');
console.log(errors2); // ["Value must be a valid email address.", "Value must be at least 8 characters long."]
現在,我們的Validator
類變得非常干凈。它不關心具體的校驗邏輯是什么,它只負責管理和執行一個ValidationStrategy
的列表。如果未來需要增加一種新的“必須是大寫”的校驗規則,我們只需要在strategies
對象中增加一個新的策略即可,完全不需要修改Validator
類。系統變得極其靈活和可擴展。
結論:設計模式是“內功心法”
我們今天“翻譯”的三個設計模式,只是冰山一-角。但它們揭示了一個核心道理:
設計模式不是讓你去“學”的條條框框,而是讓你在遇到特定問題時,能從“工具箱”里拿出來用的“內功心法”。
- 當你發現一個對象的狀態變化,需要通知多個不相關的其他對象時,你的腦中應該浮現出**“觀察者模式”**。
- 當你想在不侵入原有代碼的前提下,為多個函數或對象添加通用功能時,你的腦中應該浮現出**“裝飾器模式”**(在高階函數的世界里)。
- 當你發現一大堆
if...else
或switch
在根據不同條件執行不同算法時,你的腦中應該浮現出**“策略模式”**。
有意識地去識別這些場景,并用相應的設計模式去重構和優化你的代碼,是從一個普通的“代碼實現者”,成長為一名能夠構建大型、健壯、可維護系統的“軟件工程師”的關鍵一步。
核心要點:
- 設計模式是解決常見問題的、經過驗證的、可復用的思想和方案。
- 觀察者模式是前端響應式系統的核心,它通過解耦“主題”和“觀察者”,實現了強大的數據驅動能力。
- 裝飾器模式在JavaScript中通常通過高階函數來實現,它能在不修改原函數的情況下,為其動態添加功能。
- 策略模式通過將不同的算法封裝成獨立的“策略”對象,來消除冗長的
if...else
,讓系統更易于擴展。 - 學習設計模式,重點在于理解其解決的問題和背后的思想,并學會在現代前端的語境下,用更函數式、更簡潔的方式去“翻譯”和應用它。
在下一章 《自動化篇:用GitHub Actions打造你的“私人前端CI/CD流水線”》 中,我們將把視野從代碼本身,擴展到整個研發流程的自動化。我們將學習如何編寫一個.yml
文件,讓GitHub在我們的代碼提交時,自動地為我們完成測試、構建甚至發布等一系列工作。敬請期待!