设计模式篇:在前端,我们如何“重构”观察者、策略和装饰器模式

引子:代码里“似曾相识”的场景

作为开发者,我们总会遇到一些“似曾相识”的场景:

  • “当这个数据变化时,我需要通知其他好几个地方都更新一下。”
  • “这里有一大堆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…