当前位置: 首页 > news >正文

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

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

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

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

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

相关文章:

  • 蓝桥杯----串口
  • 内存、硬盘与缓存的技术原理及特性解析
  • 《软件测试与质量控制》实验报告二 单元测试
  • Ubuntu系统VScode实现opencv(c++)视频及摄像头使用
  • 空间平面旋转与xoy平行
  • 【BTC】挖矿
  • MyBatisPlus之CRUD接口(IService与BaseMapper)
  • 【软考中级网络工程师】知识点之堆叠
  • 公网服务器上Nginx或者Openresty如何屏蔽IP直接扫描
  • CS课程项目设计7:基于Canvas交互友好的五子棋游戏
  • 小智服务器Java安装编译(xinnan-tech)版
  • 【05】OpenCV C#——OpenCvSharp 图像基本操作---转灰度图、边缘提取、兴趣区域ROI,图像叠加
  • 28Rsync免密传输与定时备份
  • 【Spring Boot 快速入门】五、文件上传
  • 图漾相机-ROS1_SDK_ubuntu 4.X.X版本编译
  • Shell【脚本 02】离线安装配置Zookeeper及Kafka并添加service服务和开机启动(脚本分析)
  • [硬件电路-122]:模拟电路 - 信号处理电路 - 模拟电路与数字电路、各自的面临的难题对比?
  • [硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
  • 编程与数学 03-002 计算机网络 20_计算机网络课程实验与实践
  • filezilla出现connected refused的时候排查问题
  • Flink2.0学习笔记:Stream API 窗口
  • 鸿蒙智选携手IAM进驻长隆熊猫村,为国宝打造智慧健康呼吸新空间
  • 智能合约漏洞导致的损失,法律责任应如何分配
  • Hyperliquid:揭秘高性能区块链共识引擎HyperBFT
  • 入门MicroPython+ESP32:《点亮LED灯》
  • 1.7vue声明周期
  • Token系列 - 再谈稳定币
  • 保证金率(Margin Ratio)
  • 【最新区块链论文录用资讯】CCF A--WWW 2025 23篇
  • WebForms 简介