设计模式篇:在前端,我们如何“重构”观察者、策略和装饰器模式
引子:代码里“似曾相识”的场景
作为开发者,我们总会遇到一些“似曾相识”的场景:
- “当这个数据变化时,我需要通知其他好几个地方都更新一下。”
- “这里有一大堆
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在我们的代码提交时,自动地为我们完成测试、构建甚至发布等一系列工作。敬请期待!