設計模式篇:在前端,我們如何“重構”觀察者、策略和裝飾器模式

設計模式篇:在前端,我們如何“重構”觀察者、策略和裝飾器模式

引子:代碼里“似曾相識”的場景

作為開發者,我們總會遇到一些“似曾相識”的場景:

  • “當這個數據變化時,我需要通知其他好幾個地方都更新一下。”
  • “這里有一大堆if...else,根據不同的條件執行不同的邏輯,丑陋又難以擴展。”
  • “我需要給好幾個函數都增加一個相同的功能,比如記錄日志或檢查權限,但我不想去修改這些函數本身。”

這些場景,就像是編程世界里的“常見病”。而設計模式(Design Patterns),就是由前人總結出的、針對這些“常見病”的、經過千錘百煉的“經典藥方”。

然而,很多前端開發者一提到設計模式,可能會覺得它很“后端”、很“學院派”,充滿了復雜的UML圖和抽象的Java/C++示例,與我們日常用JavaScript/TypeScript構建的動態、響應式的世界格格不入。

這是一個巨大的誤解。

設計模式并非僵化的代碼模板,它是一種思想,一種解決特定問題的思路和詞匯。事實上,那些經典的GoF(《設計模式:可復用面向對象軟件的基礎》一書的四位作者)設計模式,早已化作“DNA”,深深地融入了現代前端框架和最佳實踐的血液里。只是它們換了一副更符合函數式、組件化編程思想的“面孔”。

今天,我們不當“考古學家”,去研究那些原始的、基于類的設計模式定義。我們將當一名“翻譯家”和“重構師”,帶著現代前端的視角,去重新發現和“重構”我們身邊最常見、最實用的三個設計模式:觀察者模式裝飾器模式策略模式

你將看到,這些經典思想是如何在我們之前的代碼中“靈魂附體”的,以及我們如何能有意識地運用它們,寫出更優雅、更靈活、更具可擴展性的代碼。


第一幕:觀察者模式 - “你變了,我會知道”

模式定義:觀察者模式(Observer Pattern)定義了一種一對多的依賴關系,讓多個觀察者對象(Observer)同時監聽某一個主題對象(Subject)。當主題對象的狀態發生變化時,它會通知所有觀察者,使它們能夠自動更新自己。

這聽起來是不是無比熟悉?沒錯,它就是我們這個系列中反復出現的核心思想:響應式數據驅動的基石。

場景重現:我們的“發布/訂閱”和“狀態機”

  1. 發布/訂閱模式 (EventBus)
    在我們的第十章中,我們構建了一個類型安全的事件總線。

    • 主題(Subject): EventBus實例本身。
    • 觀察者(Observer): 通過bus.on('eventName', callback)注冊的每一個callback函數。
    • 通知(Notify): 當調用bus.emit('eventName', payload)時,EventBus遍歷并執行所有監聽'eventName'callback
  2. Redux-like狀態機 (createStore)
    在我們的第五章中,我們實現了一個createStore函數。

    • 主題(Subject): store實例。
    • 觀察者(Observer): 通過store.subscribe(listener)注冊的每一個listener函數。
    • 通知(Notify): 在store.dispatch(action)導致state更新后,store會遍歷并執行所有的listener

觀察者模式的核心,是解耦。主題對象(如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...elseswitch在根據不同條件執行不同算法時,你的腦中應該浮現出**“策略模式”**。

有意識地去識別這些場景,并用相應的設計模式去重構和優化你的代碼,是從一個普通的“代碼實現者”,成長為一名能夠構建大型、健壯、可維護系統的“軟件工程師”的關鍵一步。

核心要點:

  1. 設計模式是解決常見問題的、經過驗證的、可復用的思想和方案
  2. 觀察者模式是前端響應式系統的核心,它通過解耦“主題”和“觀察者”,實現了強大的數據驅動能力。
  3. 裝飾器模式在JavaScript中通常通過高階函數來實現,它能在不修改原函數的情況下,為其動態添加功能。
  4. 策略模式通過將不同的算法封裝成獨立的“策略”對象,來消除冗長的if...else,讓系統更易于擴展。
  5. 學習設計模式,重點在于理解其解決的問題和背后的思想,并學會在現代前端的語境下,用更函數式、更簡潔的方式去“翻譯”和應用它。

在下一章 《自動化篇:用GitHub Actions打造你的“私人前端CI/CD流水線”》 中,我們將把視野從代碼本身,擴展到整個研發流程的自動化。我們將學習如何編寫一個.yml文件,讓GitHub在我們的代碼提交時,自動地為我們完成測試、構建甚至發布等一系列工作。敬請期待!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91691.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91691.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91691.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Node.js 服務可以實現哪些功能

以下是 Node.js 服務可以實現的 100 個功能&#xff0c;涵蓋 Web 開發、工具鏈、系統集成、自動化等方向&#xff0c;按類別分類整理&#xff1a;一、Web 開發相關 RESTful API 服務GraphQL 服務實時聊天應用&#xff08;WebSocket/Socket.IO&#xff09;博客/CMS 系統電子商務…

如何安裝和使用 Cursor AI 編輯器

在軟件開發領域&#xff0c;幾乎每天都有新工具涌現&#xff0c;找到最適合您工作流程的工具可能會改變游戲規則。Cursor 是一款 AI 驅動的代碼編輯器&#xff0c;其革命性的 API 管理插件 EchoAPI 就是其中的代表。它們強強聯手&#xff0c;承諾在一個強大的平臺內簡化您的編碼…

LangChain框架概念及簡單的使用案例

一、LangChain介紹LangChain是一個強大的用于開發大模型應用程序的框架&#xff0c;為開發提供豐富的工具和組件&#xff0c;使得構造復雜的自然語言處理變得更加高效和便捷。它允許開發者將大語言模型與其他數據源工具集成&#xff0c;從而創建出能處理各種任務的智能體應用&a…

安卓audio 架構解析

audio_port_handle_t ? 定義&#xff1a;audio_port_handle_t標識音頻設備&#xff08;如揚聲器、耳機&#xff09;或虛擬端口&#xff08;如遠程 submix&#xff09;。它在設備連接或策略路由時由AudioPolicyManager分配&#xff0c;例如通過setDeviceConnectionState()動態注…

GitHub 上 Star 數量前 8 的開源 MCP 項目

原文鏈接&#xff1a;https://www.nocobase.com/cn/blog/github-open-source-mcp-projects。 MCP 這個詞真正被廣泛提起&#xff0c;是在 2025 年年初&#xff0c;尤其是在 AI 工具開發圈。3 月&#xff0c;一場圍繞 “MCP 是否能成為未來標準協議” 的爭論徹底點燃了討論熱度…

【數據結構與算法】數據結構初階:排序內容加餐(二)——文件歸并排序思路詳解(附代碼實現)

&#x1f525;個人主頁&#xff1a;艾莉絲努力練劍 ?專欄傳送門&#xff1a;《C語言》、《數據結構與算法》、C語言刷題12天IO強訓、LeetCode代碼強化刷題 &#x1f349;學習方向&#xff1a;C/C方向 ??人生格言&#xff1a;為天地立心&#xff0c;為生民立命&#xff0c;為…

Jetson Orin NX/NANO+ubuntu22.04+humble+MAVROS2安裝教程

MAVROS2目前不是官方提供的標準&#xff0c;主要區別還是通信機制的不同&#xff0c;以及API接口的區別&#xff0c;在使用的過程中&#xff0c;根據對應的版本安裝即可&#xff0c;此處進提供簡易的二進制安裝方法&#xff0c;源碼安裝暫不提供&#xff0c;前去使用mavros即可…

Ubuntu 安裝 ns-3 教程

Ubuntu 安裝 ns-3最全 教程 1. 環境更新 sudo apt update sudo apt install git2. Ns3 最低依賴要求 2.1 安裝依賴 安裝依賴網址&#xff1a;根據自己安裝的版本安裝對應依賴。 https://www.nsnam.org/wiki/Installation Ubuntu/Debian/Mint 以下軟件包列表在 Ubuntu 22.…

《林景媚與命運解放者》

《林景媚與命運解放者》——當數據庫成為命運的主宰&#xff0c;誰將成為人類自由意志的解放者&#xff1f;《林景媚數據庫宇宙》系列第十二部第一章&#xff1a;解放者的召喚公元 2098 年&#xff0c;隨著“命運終結者”的威脅被解除&#xff0c;PostgreSQL Quantum Engine&am…

linux編譯基礎知識-頭文件標準路徑

&#x1f4c2; ??1. 系統路徑結構差異?? 要查看 GCC 的默認頭文件搜索路徑&#xff0c;可通過以下方法操作&#xff08;以 Linux 環境為例&#xff09;&#xff1a; ??1. 查看 C 語言頭文件路徑?? gcc -v -E -xc - < /dev/null 2>&1 | grep -A 100 "#in…

離線語音芯片有哪些品牌和型號?

離線語音芯片的品牌有很多&#xff0c;型號也有很多&#xff0c;因為離線語音芯片的市場很大&#xff0c;幾乎所有的想要語音控制的產品都可以通過增加一顆離線語音芯片來實現語音控制的能力&#xff0c;今天主要提到的就是離線語音芯片品牌廠家之一的唯創知音。唯創知音發展歷…

Linux 軟件包管理

Linux 軟件包管理 分析 RPM 包 Linux 發行版本以 RHEL 為代表的發行版本&#xff0c;使用rpm包管理系統&#xff1a; RHEL (Red Hat Enterprise Linux&#xff09;Fedora&#xff08;由原來的RedHat桌面版本發展而來&#xff0c;免費版本&#xff09;CentOS&#xff08;RHEL的…

使用 Vue 3.0 Composition API 優化流程設計器界面

&#x1f90d; 前端開發工程師、技術日更博主、已過CET6 &#x1f368; 阿珊和她的貓_CSDN博客專家、23年度博客之星前端領域TOP1 &#x1f560; 牛客高級專題作者、打造專欄《前端面試必備》 、《2024面試高頻手撕題》、《前端求職突破計劃》 &#x1f35a; 藍橋云課簽約作者、…

2025Nacos安裝Mac版本 少走彎路版本

https://github.com/alibaba/nacos 一開始看網上文章&#xff0c;隨便下了一個最新的3.0.2&#xff0c;然后出現很多錯誤 密鑰等等問題&#xff0c;最后啟動了&#xff0c;但是打不開鏈接&#xff1a;http://localhost:8848/nacos 然后開始找問題日志&#xff0c;/.nofollow/…

sifu mod制作 相關經驗

sifu mod制作一遍流程數據傳遞后拆開是ok的&#xff0c;沒必要合并 斷片不能使用原材質不然導入ue里沒法片段選擇 效果拔群 帶自動權重就會有跟隨骨骼的效果&#xff0c;空頂點組會跟隨父級的原點 這個選負的會抵消膠囊的碰撞效果 應用并刷新布料模擬&#xff08;相當于工程圖的…

論文精讀筆記:Overview

本文檔記錄了一些經典論文的講解筆記。 重讀經典&#xff1a;《ImageNet Classification with Deep Convolutional Neural Networks》 重讀經典&#xff1a;《Generative Adversarial Nets》 重讀經典&#xff1a;《Deep Residual Learning for Image Recognition》 重讀經典…

Elasticsearch+Logstash+Filebeat+Kibana單機部署

目錄 一、配置準備 下載java&#xff0c;需要java環境 二、單機模式 ELK部署 修改域名解析 elasticsearch配置 啟動elasticsearch服務 查看是否啟用 查看監聽端口 logstash服務 創建配置文件 kibana 啟動服務kebana 驗證 網頁訪問 ?編輯 生成圖表 回到網頁 一、配置準…

redis快速部署、集成、調優

redis快速部署、集成、調優 1.部署 1.1 docker部署 參考&#xff1a;https://blog.csdn.net/taotao_guiwang/article/details/135508643 1.2 redis部署 資源見&#xff0c;百度網盤&#xff1a;https://pan.baidu.com/s/1qlabJ7m8BDm77GbDuHmbNQ?pwd41ac 執行redis_insta…

大學生HTML期末大作業——HTML+CSS+JavaScript音樂網站

HTMLCSSJS【音樂網站】網頁設計期末課程大作業 web前端開發技術 web課程設計 網頁規劃與設計&#x1f4a5; 文章目錄一、&#x1f3c1; 網站題目二、&#x1f6a9; 網站描述三、&#x1f38c; 網站介紹四、&#x1f3f4; 網站效果五、&#x1f3f3;? 網站代碼六、&#x1f3f3…

ARP協議是什么?ARP欺騙是如何實現的?我們該如何預防ARP欺騙?

ARP&#xff08;Address Resolution Protocol&#xff0c;地址解析協議&#xff09;是一個工作在數據鏈路層&#xff08;OSI第二層&#xff09;和網絡層&#xff08;OSI第三層&#xff09;之間的基礎網絡協議&#xff0c;它的核心功能是將網絡層地址&#xff08;IP地址&#xf…