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

发布于:2025-08-03 ⋅ 阅读:(11) ⋅ 点赞:(0)

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

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

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

  • “当这个数据变化时,我需要通知其他好几个地方都更新一下。”
  • “这里有一大堆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在我们的代码提交时,自动地为我们完成测试、构建甚至发布等一系列工作。敬请期待!


网站公告

今日签到

点亮在社区的每一天
去签到