前端的设计模式?我觉得90%都是在过度设计!

最近Code Review的时候,我看到我们组一个很聪明的年轻同事,用观察者模式,写了一个极其复杂的全局状态订阅系统,就为了在一个组件里,响应另一个不相关的组件的点击事件。

比较常见的场景:点击 Button 组件,让 Panel 组件打印日志或显示提示,具体伪代码👇:

js 复制代码
// observer.js
class Observer {
  constructor() {
    this.subscribers = [];
  }

  subscribe(fn) {
    this.subscribers.push(fn);
  }

  unsubscribe(fn) {
    this.subscribers = this.subscribers.filter(sub => sub !== fn);
  }

  notify(data) {
    this.subscribers.forEach(fn => fn(data));
  }
}

// 全局状态中心(相当于单例)
export const globalClickObserver = new Observer();
jsx 复制代码
// Button.jsx
import React from "react";
import { globalClickObserver } from "./observer";

export default function Button() {
  const handleClick = () => {
    console.log("Button clicked");
    globalClickObserver.notify({ source: "Button", payload: "Hello Panel" });
  };

  return <button onClick={handleClick}>Click</button>;
}
jsx 复制代码
// Panel.jsx
import React, { useEffect } from "react";
import { globalClickObserver } from "./observer";

export default function Panel() {
  useEffect(() => {
    const subscriber = (data) => {
      if (data.source === "Button") {
        console.log("event:", data.payload);
      }
    };

    globalClickObserver.subscribe(subscriber);
    return () => globalClickObserver.unsubscribe(subscriber);
  }, []);

  return <div>I'm Panel</div>;
}

我把他叫过来,问他为什么不直接用一个简单的Event Bus(比如mitt),或者干脆用Zustand这样的状态管理器。

他说:"我觉得用设计模式,代码的扩展性会更好,也显得更高级😂。"

这个瞬间,让我下定决心,想聊聊这个话题:

在现代前端开发(尤其是React/Vue)中,我们挂在嘴边的那些经典设计模式,90%都是在过度设计。

在我开喷之前,请允许我澄清:我反对的不是设计思想,比如 高内聚低耦合单一职责。我反对的是,把那些20年前为Java/C++总结的、沉重的、面向对象的大招,生搬硬套到我们现代前端的开发范式里。


我们为什么会陷入设计模式的陷阱?

曾几何时,我也曾是设计模式的忠实信徒。热衷于在代码里寻找应用工厂模式、策略模式的场景。

我们之所以会这样,我觉得原因有二:

为了应对面试

设计模式是前端面试八股文里的重灾区。为了通过面试,我们不得不去背诵它们的定义和用法,这就导致了一种为了应考的惯性思维。

看起来牛皮🤷‍♂️

我们总觉得,能说出几个设计模式的名字,能把它们用在代码里,就代表自己的水平更高。仿佛不说个单例、不聊个装饰器,就体现不出自己的资深。


有哪些水土不服的设计模式?

我们来看几个在前端领域最常被滥用的经典模式。

单例模式

经典写法 :搞一个class,一个私有构造函数,再加一个getInstance的静态方法,防止被多次new

我的吐槽点别闹了,我们有ES6模块!!!

前端的原生模式 :JavaScript的import/export机制,天生就是单例的。你export一个实例,在所有地方import它,它从始至终就是同一个实例。

javascript 复制代码
// a.js
class MyService { /* ... */ }
// 导出一个实例
export const myServiceInstance = new MyService();

// b.js
import { myServiceInstance } from './a.js';
// c.js
import { myServiceInstance } from './a.js';
// b.js和c.js里的myServiceInstance,是同一个东西

为了实现单例,而去手写一个Singleton类,在现代前端里,属于(省略一万字...)。

工厂模式

写法 :写一个create函数,根据传入的typenew出不同的类的实例 ?。

在React/Vue里,我们有比工厂更强大、更直观的武器------组件 。你根本不需要一个create函数,你只需要一个组件 ,通过props来决定它的形态和行为。

jsx 复制代码
// 你不需要一个 createButton 的工厂
// 你只需要一个 Button 组件
function Button({ kind, ...props }) {
  if (kind === 'icon') {
    return <IconButton {...props} />;
  }
  if (kind === 'text') {
    return <TextButton {...props} />;
  }
  return <PrimaryButton {...props} />;
}

用组件思维去思考,比工厂思维更符合现代前端的直觉。

观察者模式

写法 :维护一个订阅者列表(subscribers),提供subscribeunsubscribenotify方法?

我的吐槽点是你的框架自带的响应式系统,比你手写的强一百倍。

React的useState/useEffect,Vue的ref/watch,它们本身就是更高阶、更强大的响应式系统,是观察者模式的终极体现。状态(被观察者)变化,UI(观察者)自动更新。你为什么要去手写一个简陋版的伪响应式,而不用框架自带的、经过千锤百炼的完整经验呢?


那剩下 10%有用的,是什么?

我喷了90%,那剩下10%依然有价值的是什么?在我看来,是一些设计思想,而不是具体的什么大招。

发布/订阅模式 (Pub/Sub)

它和观察者模式很像,但更解耦。当两个完全不相关的组件需要通信,而你又不想为此引入一个全局状态库时,一个轻量级的事件总线 (Event Bus)或者 mitt,就非常有用。

javascript 复制代码
// pubsub.js
class PubSub {
  constructor() {
    this.events = {}; // 存储事件和对应的订阅者回调
  }

  // 订阅
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return () => this.unsubscribe(event, callback); // 返回取消订阅函数
  }

  // 取消订阅
  unsubscribe(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  // 发布
  publish(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => callback(data));
  }
}

// 导出一个全局单例
export const pubsub = new PubSub();

策略模式 : 这个模式的核心思想------将不同的算法封装起来,使它们可以互相替换------在前端依然非常闪光。它能帮助我们写出更优雅、更易扩展的代码,用来代替冗长的if/elseswitch

javascript 复制代码
// 比如,处理不同类型的用户折扣
const strategies = {
  'normal': (price) => price,
  'vip': (price) => price * 0.8,
  'svip': (price) => price * 0.6,
};

function calculatePrice(userType, price) {
  return strategies[userType](price);
}

你看,这里没有class,没有那么复杂的逻辑,但它蕴含了策略模式的思想。


作为组长,当我在Code Review 里看到一个同事用了工厂模式时,我不会觉得他很牛逼。我反而会警惕:他是不是为了炫技?而选择了一个更复杂的方案?我们能不能用一个简单的React组件,就把这事儿给干了?

现代前端框架,已经为我们内建了一套非常优秀、非常自洽的设计模式。组件是工厂,Hooks是装饰器/策略,响应式系统是观察者。

你的目标,不是写出能套上某个设计模式名字的代码,而是写出简单、清晰、易于维护的代码。在前端,后者往往比前者重要得多🤷‍♂️。

相关推荐
Miloce2 小时前
零成本搭建跨域代理服务 - Cloudflare Workers实战指南
前端
叫我詹躲躲3 小时前
🌟 回溯算法原来这么简单:10道经典题,一看就明白!
前端·算法·leetcode
薄雾晚晴3 小时前
大屏实战:ECharts 自适应,用 ResizeObserver 解决容器尺寸变化难题
前端·javascript·vue.js
爱分享的鱼鱼3 小时前
为什么使用express框架
前端·后端
资源开发与学习3 小时前
从0到1,LangChain+RAG全链路实战AI知识库
前端·人工智能
我是天龙_绍3 小时前
面试官:给我实现一个图片标注工具,截图标注,讲一下思路
前端
喵桑丶3 小时前
无界(微前端框架)
前端·javascript
leeggco3 小时前
AI数字人可视化图表设计文档
前端
我是天龙_绍3 小时前
仿一下微信的截图标注功能
前端