03-行为型模式

15 阅读17分钟

第三部分:行为型模式

行为型模式关注对象之间的通信,帮助你实现更灵活的交互逻辑

目录


观察者模式

💡 模式定义

观察者模式(Observer Pattern)定义对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

🤔 为什么需要观察者模式?

问题场景:多个组件需要响应同一个数据变化

假设你的电商应用有购物车功能,当购物车商品数量变化时,需要更新多个地方:

❌ 不使用观察者模式的痛点
// CartManager.js - 购物车管理器
class CartManager {
  constructor() {
    this.items = [];
  }

  addItem(item) {
    this.items.push(item);

    // 耦合:直接调用各个组件的更新方法
    updateCartBadge(this.items.length);
    updateCartSidebar(this.items);
    updateCheckoutButton(this.items.length > 0);
    updateTotalPrice(this.calculateTotal());

    // 每次添加新功能都要修改这里!
    // updateRecommendations(this.items);
    // updateCouponHints(this.items);
  }

  removeItem(itemId) {
    this.items = this.items.filter(item => item.id !== itemId);

    // 又要重复调用所有更新方法
    updateCartBadge(this.items.length);
    updateCartSidebar(this.items);
    updateCheckoutButton(this.items.length > 0);
    updateTotalPrice(this.calculateTotal());
  }

  calculateTotal() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}

// 各个独立的更新函数
function updateCartBadge(count) {
  document.querySelector('.cart-badge').textContent = count;
}

function updateCartSidebar(items) {
  // 更新侧边栏
}

function updateCheckoutButton(enabled) {
  document.querySelector('.checkout-btn').disabled = !enabled;
}

function updateTotalPrice(total) {
  document.querySelector('.total-price').textContent = ${total}`;
}

问题

  • 紧耦合:CartManager 必须知道所有需要更新的组件
  • 难以扩展:新增功能需要修改 CartManager
  • 难以维护:一个地方改动可能影响多个功能
  • 违反单一职责:CartManager 既管理数据又负责通知

✅ 使用观察者模式解决
// EventEmitter.js - 事件发射器(观察者模式实现)
class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  // 订阅事件(注册观察者)
  on(event, listener) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event).push(listener);

    // 返回取消订阅函数
    return () => this.off(event, listener);
  }

  // 取消订阅
  off(event, listener) {
    if (!this.events.has(event)) return;

    const listeners = this.events.get(event);
    const index = listeners.indexOf(listener);
    if (index > -1) {
      listeners.splice(index, 1);
    }
  }

  // 触发事件(通知所有观察者)
  emit(event, ...args) {
    if (!this.events.has(event)) return;

    const listeners = this.events.get(event);
    listeners.forEach(listener => {
      try {
        listener(...args);
      } catch (error) {
        console.error(`Error in event listener for ${event}:`, error);
      }
    });
  }

  // 单次订阅
  once(event, listener) {
    const onceWrapper = (...args) => {
      listener(...args);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
  }
}

export default EventEmitter;
// CartManager.js - 购物车管理器(发布者)
import EventEmitter from './EventEmitter';

class CartManager extends EventEmitter {
  constructor() {
    super();
    this.items = [];
  }

  addItem(item) {
    this.items.push(item);

    // 只负责发布事件,不关心谁在监听
    this.emit('itemAdded', item);
    this.emit('cartChanged', this.items);
  }

  removeItem(itemId) {
    const removedItem = this.items.find(item => item.id === itemId);
    this.items = this.items.filter(item => item.id !== itemId);

    this.emit('itemRemoved', removedItem);
    this.emit('cartChanged', this.items);
  }

  updateQuantity(itemId, quantity) {
    const item = this.items.find(item => item.id === itemId);
    if (item) {
      item.quantity = quantity;
      this.emit('quantityChanged', item);
      this.emit('cartChanged', this.items);
    }
  }

  clear() {
    this.items = [];
    this.emit('cartCleared');
    this.emit('cartChanged', this.items);
  }

  getItems() {
    return this.items;
  }

  calculateTotal() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}

export default new CartManager();
// CartBadge.jsx - 购物车徽章组件(观察者)
import React from 'react';
import cartManager from './CartManager';

function CartBadge() {
  const [count, setCount] = React.useState(cartManager.getItems().length);

  React.useEffect(() => {
    // 订阅购物车变化
    const unsubscribe = cartManager.on('cartChanged', (items) => {
      setCount(items.length);
    });

    // 清理订阅
    return unsubscribe;
  }, []);

  return (
    <div className="cart-badge">
      <span className="icon">🛒</span>
      {count > 0 && <span className="count">{count}</span>}
    </div>
  );
}

export default CartBadge;
// CartSidebar.jsx - 购物车侧边栏组件(观察者)
import React from 'react';
import cartManager from './CartManager';

function CartSidebar() {
  const [items, setItems] = React.useState(cartManager.getItems());
  const [total, setTotal] = React.useState(cartManager.calculateTotal());

  React.useEffect(() => {
    const unsubscribe = cartManager.on('cartChanged', (newItems) => {
      setItems(newItems);
      setTotal(cartManager.calculateTotal());
    });

    return unsubscribe;
  }, []);

  return (
    <div className="cart-sidebar">
      <h3>购物车</h3>
      {items.length === 0 ? (
        <p>购物车是空的</p>
      ) : (
        <>
          <ul>
            {items.map(item => (
              <li key={item.id}>
                {item.name} x {item.quantity} - ¥{item.price * item.quantity}
                <button onClick={() => cartManager.removeItem(item.id)}>删除</button>
              </li>
            ))}
          </ul>
          <div className="total">总计: ¥{total}</div>
          <button className="checkout-btn">去结算</button>
        </>
      )}
    </div>
  );
}

export default CartSidebar;
// ProductCard.jsx - 商品卡片组件
import React from 'react';
import cartManager from './CartManager';

function ProductCard({ product }) {
  const handleAddToCart = () => {
    cartManager.addItem({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1,
    });
  };

  return (
    <div className="product-card">
      <h4>{product.name}</h4>
      <p>¥{product.price}</p>
      <button onClick={handleAddToCart}>加入购物车</button>
    </div>
  );
}

export default ProductCard;
// Analytics.js - 数据分析(观察者)
import cartManager from './CartManager';

class Analytics {
  constructor() {
    // 订阅购物车事件
    cartManager.on('itemAdded', (item) => {
      this.trackEvent('cart_item_added', {
        item_id: item.id,
        item_name: item.name,
        price: item.price,
      });
    });

    cartManager.on('itemRemoved', (item) => {
      this.trackEvent('cart_item_removed', {
        item_id: item.id,
      });
    });
  }

  trackEvent(eventName, data) {
    console.log('Analytics:', eventName, data);
    // 发送到分析服务器
  }
}

export default new Analytics();

效果

  • ✅ 解耦:CartManager 不知道谁在监听
  • ✅ 易于扩展:新增功能只需订阅事件
  • ✅ 单一职责:每个组件只关心自己的逻辑
  • ✅ 动态订阅:可以随时添加/移除观察者

🎯 React 中的观察者模式

使用 Context + useReducer 实现
// CartContext.jsx - 使用 Context 实现观察者模式
import React from 'react';

const CartContext = React.createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload),
      };
    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        ),
      };
    case 'CLEAR':
      return {
        ...state,
        items: [],
      };
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = React.useReducer(cartReducer, { items: [] });

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: itemId });
  };

  const updateQuantity = (itemId, quantity) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id: itemId, quantity } });
  };

  const clear = () => {
    dispatch({ type: 'CLEAR' });
  };

  const calculateTotal = () => {
    return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  };

  return (
    <CartContext.Provider value={{
      items: state.items,
      addItem,
      removeItem,
      updateQuantity,
      clear,
      calculateTotal,
    }}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => {
  const context = React.useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within CartProvider');
  }
  return context;
};
// 使用 Context
function App() {
  return (
    <CartProvider>
      <Header />
      <ProductList />
      <CartSidebar />
    </CartProvider>
  );
}

function CartBadge() {
  const { items } = useCart(); // 自动订阅 items 变化

  return (
    <div className="cart-badge">
      <span className="icon">🛒</span>
      {items.length > 0 && <span className="count">{items.length}</span>}
    </div>
  );
}

function ProductCard({ product }) {
  const { addItem } = useCart();

  return (
    <div className="product-card">
      <h4>{product.name}</h4>
      <p>¥{product.price}</p>
      <button onClick={() => addItem({ ...product, quantity: 1 })}>
        加入购物车
      </button>
    </div>
  );
}

🏗️ 真实业务场景

场景1:实时通知系统
// NotificationManager.js - 通知管理器
import EventEmitter from './EventEmitter';

class NotificationManager extends EventEmitter {
  constructor() {
    super();
    this.notifications = [];
  }

  // 显示通知
  show(notification) {
    const id = Date.now();
    const notif = {
      id,
      ...notification,
      timestamp: new Date(),
    };

    this.notifications.push(notif);
    this.emit('notificationAdded', notif);

    // 自动关闭
    if (notification.duration) {
      setTimeout(() => {
        this.dismiss(id);
      }, notification.duration);
    }

    return id;
  }

  // 关闭通知
  dismiss(id) {
    const notification = this.notifications.find(n => n.id === id);
    if (notification) {
      this.notifications = this.notifications.filter(n => n.id !== id);
      this.emit('notificationDismissed', notification);
    }
  }

  // 清空所有通知
  clearAll() {
    this.notifications = [];
    this.emit('notificationsCleared');
  }

  // 便捷方法
  success(message, duration = 3000) {
    return this.show({ type: 'success', message, duration });
  }

  error(message, duration = 5000) {
    return this.show({ type: 'error', message, duration });
  }

  warning(message, duration = 4000) {
    return this.show({ type: 'warning', message, duration });
  }

  info(message, duration = 3000) {
    return this.show({ type: 'info', message, duration });
  }
}

export default new NotificationManager();
// NotificationContainer.jsx - 通知容器组件
import React from 'react';
import notificationManager from './NotificationManager';

function NotificationContainer() {
  const [notifications, setNotifications] = React.useState([]);

  React.useEffect(() => {
    const handleAdded = (notification) => {
      setNotifications(prev => [...prev, notification]);
    };

    const handleDismissed = (notification) => {
      setNotifications(prev => prev.filter(n => n.id !== notification.id));
    };

    const handleCleared = () => {
      setNotifications([]);
    };

    notificationManager.on('notificationAdded', handleAdded);
    notificationManager.on('notificationDismissed', handleDismissed);
    notificationManager.on('notificationsCleared', handleCleared);

    return () => {
      notificationManager.off('notificationAdded', handleAdded);
      notificationManager.off('notificationDismissed', handleDismissed);
      notificationManager.off('notificationsCleared', handleCleared);
    };
  }, []);

  return (
    <div className="notification-container">
      {notifications.map(notification => (
        <Notification
          key={notification.id}
          notification={notification}
          onClose={() => notificationManager.dismiss(notification.id)}
        />
      ))}
    </div>
  );
}

function Notification({ notification, onClose }) {
  const typeStyles = {
    success: { background: '#52c41a', color: '#fff' },
    error: { background: '#ff4d4f', color: '#fff' },
    warning: { background: '#faad14', color: '#fff' },
    info: { background: '#1890ff', color: '#fff' },
  };

  return (
    <div className="notification" style={typeStyles[notification.type]}>
      <span>{notification.message}</span>
      <button onClick={onClose}>×</button>
    </div>
  );
}

export default NotificationContainer;
// 在应用中使用
import notificationManager from './NotificationManager';

function LoginForm() {
  const handleLogin = async (credentials) => {
    try {
      await loginAPI(credentials);
      notificationManager.success('登录成功!');
    } catch (error) {
      notificationManager.error('登录失败:' + error.message);
    }
  };

  // ...
}
场景2:WebSocket 实时数据
// WebSocketManager.js - WebSocket 管理器
import EventEmitter from './EventEmitter';

class WebSocketManager extends EventEmitter {
  constructor() {
    super();
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
  }

  connect(url) {
    this.ws = new WebSocket(url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.emit('connected');
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);

        // 根据消息类型触发不同事件
        switch (data.type) {
          case 'user_online':
            this.emit('userOnline', data.payload);
            break;
          case 'user_offline':
            this.emit('userOffline', data.payload);
            break;
          case 'new_message':
            this.emit('newMessage', data.payload);
            break;
          case 'order_status_changed':
            this.emit('orderStatusChanged', data.payload);
            break;
          default:
            this.emit('message', data);
        }
      } catch (error) {
        console.error('Failed to parse WebSocket message:', error);
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.emit('error', error);
    };

    this.ws.onclose = () => {
      console.log('WebSocket disconnected');
      this.emit('disconnected');
      this.reconnect(url);
    };
  }

  reconnect(url) {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

      setTimeout(() => {
        this.connect(url);
      }, 1000 * this.reconnectAttempts);
    } else {
      this.emit('reconnectFailed');
    }
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error('WebSocket is not connected');
    }
  }

  disconnect() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

export default new WebSocketManager();
// OnlineUsers.jsx - 在线用户列表
import React from 'react';
import wsManager from './WebSocketManager';

function OnlineUsers() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    const handleUserOnline = (user) => {
      setUsers(prev => [...prev, user]);
    };

    const handleUserOffline = (user) => {
      setUsers(prev => prev.filter(u => u.id !== user.id));
    };

    wsManager.on('userOnline', handleUserOnline);
    wsManager.on('userOffline', handleUserOffline);

    return () => {
      wsManager.off('userOnline', handleUserOnline);
      wsManager.off('userOffline', handleUserOffline);
    };
  }, []);

  return (
    <div className="online-users">
      <h3>在线用户 ({users.length})</h3>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            <span className="online-indicator">🟢</span>
            {user.name}
          </li>
        ))}
      </ul>
    </div>
  );
}
// ChatMessages.jsx - 聊天消息
import React from 'react';
import wsManager from './WebSocketManager';

function ChatMessages() {
  const [messages, setMessages] = React.useState([]);

  React.useEffect(() => {
    const handleNewMessage = (message) => {
      setMessages(prev => [...prev, message]);
    };

    wsManager.on('newMessage', handleNewMessage);

    return () => {
      wsManager.off('newMessage', handleNewMessage);
    };
  }, []);

  return (
    <div className="chat-messages">
      {messages.map((msg, index) => (
        <div key={index} className="message">
          <strong>{msg.username}:</strong> {msg.text}
        </div>
      ))}
    </div>
  );
}

🎨 在主流框架中的应用

Node.js EventEmitter
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

myEmitter.on('event', (a, b) => {
  console.log(a, b);
});

myEmitter.emit('event', 'a', 'b');
Vue 响应式系统
// Vue 2/3 使用观察者模式实现响应式
export default {
  data() {
    return {
      count: 0
    };
  },
  watch: {
    // 观察 count 变化
    count(newValue, oldValue) {
      console.log('count changed:', newValue);
    }
  }
}
RxJS - 强大的观察者模式库
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

// 监听输入框变化
const input = document.querySelector('#search');
const search$ = fromEvent(input, 'input').pipe(
  debounceTime(300),
  map(event => event.target.value)
);

search$.subscribe(value => {
  console.log('搜索:', value);
});

⚖️ 优缺点分析

✅ 优点
  1. 解耦:发布者和订阅者解耦
  2. 动态关系:可以在运行时建立/解除订阅
  3. 广播通信:一对多通知
  4. 符合开闭原则:添加新观察者不需要修改发布者
❌ 缺点
  1. 性能问题:大量观察者会影响性能
  2. 内存泄漏:忘记取消订阅会导致内存泄漏
  3. 调试困难:事件流追踪困难
  4. 顺序问题:观察者执行顺序不确定

📋 何时使用观察者模式

✅ 适合使用的场景
  • 状态变化需要通知多个对象
  • 事件系统、消息总线
  • 实时数据同步(WebSocket)
  • 跨组件通信
  • 日志、分析、监控系统
❌ 不适合使用的场景
  • 只有一个订阅者
  • 同步顺序很重要
  • 性能敏感的场景
  • 简单的父子组件通信(用 props 即可)

策略模式

💡 模式定义

策略模式(Strategy Pattern)定义一系列算法,把它们封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。

🤔 为什么需要策略模式?

问题场景:表单验证、支付方式、排序算法等需要切换的逻辑
❌ 不使用策略模式的痛点
// FormValidator.jsx - 充满 if-else 的验证逻辑
function validateForm(formData, rules) {
  const errors = {};

  Object.keys(rules).forEach(field => {
    const value = formData[field];
    const rule = rules[field];

    // 大量 if-else 判断
    if (rule.required) {
      if (!value || value.trim() === '') {
        errors[field] = `${field} is required`;
        return;
      }
    }

    if (rule.minLength) {
      if (value.length < rule.minLength) {
        errors[field] = `${field} must be at least ${rule.minLength} characters`;
        return;
      }
    }

    if (rule.maxLength) {
      if (value.length > rule.maxLength) {
        errors[field] = `${field} must be at most ${rule.maxLength} characters`;
        return;
      }
    }

    if (rule.email) {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) {
        errors[field] = `${field} must be a valid email`;
        return;
      }
    }

    if (rule.phone) {
      const phoneRegex = /^1[3-9]\d{9}$/;
      if (!phoneRegex.test(value)) {
        errors[field] = `${field} must be a valid phone number`;
        return;
      }
    }

    if (rule.url) {
      try {
        new URL(value);
      } catch {
        errors[field] = `${field} must be a valid URL`;
        return;
      }
    }

    if (rule.number) {
      if (isNaN(value)) {
        errors[field] = `${field} must be a number`;
        return;
      }
    }

    if (rule.min) {
      if (Number(value) < rule.min) {
        errors[field] = `${field} must be at least ${rule.min}`;
        return;
      }
    }

    if (rule.max) {
      if (Number(value) > rule.max) {
        errors[field] = `${field} must be at most ${rule.max}`;
        return;
      }
    }

    // 每次添加新规则都要加 if-else...
  });

  return errors;
}

问题

  • 难以扩展:添加新规则需要修改核心代码
  • 代码臃肿:大量 if-else
  • 难以测试:每个规则都耦合在一起
  • 难以复用:规则无法独立使用

✅ 使用策略模式解决
// validators/strategies.js - 验证策略
const validationStrategies = {
  // 必填策略
  required: (value, errorMsg = '此字段为必填') => {
    if (!value || String(value).trim() === '') {
      return errorMsg;
    }
    return null;
  },

  // 最小长度策略
  minLength: (length, errorMsg) => (value) => {
    if (value && value.length < length) {
      return errorMsg || `最少需要 ${length} 个字符`;
    }
    return null;
  },

  // 最大长度策略
  maxLength: (length, errorMsg) => (value) => {
    if (value && value.length > length) {
      return errorMsg || `最多允许 ${length} 个字符`;
    }
    return null;
  },

  // 邮箱策略
  email: (errorMsg = '请输入有效的邮箱地址') => (value) => {
    if (!value) return null;
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(value) ? null : errorMsg;
  },

  // 手机号策略
  phone: (errorMsg = '请输入有效的手机号') => (value) => {
    if (!value) return null;
    const regex = /^1[3-9]\d{9}$/;
    return regex.test(value) ? null : errorMsg;
  },

  // 网址策略
  url: (errorMsg = '请输入有效的网址') => (value) => {
    if (!value) return null;
    try {
      new URL(value);
      return null;
    } catch {
      return errorMsg;
    }
  },

  // 数字策略
  number: (errorMsg = '请输入数字') => (value) => {
    if (!value) return null;
    return isNaN(value) ? errorMsg : null;
  },

  // 最小值策略
  min: (minValue, errorMsg) => (value) => {
    if (!value) return null;
    const num = Number(value);
    return num < minValue ? (errorMsg || `最小值为 ${minValue}`) : null;
  },

  // 最大值策略
  max: (maxValue, errorMsg) => (value) => {
    if (!value) return null;
    const num = Number(value);
    return num > maxValue ? (errorMsg || `最大值为 ${maxValue}`) : null;
  },

  // 正则表达式策略
  pattern: (regex, errorMsg = '格式不正确') => (value) => {
    if (!value) return null;
    return regex.test(value) ? null : errorMsg;
  },

  // 自定义策略
  custom: (validatorFn, errorMsg) => (value) => {
    const isValid = validatorFn(value);
    return isValid ? null : errorMsg;
  },
};

export default validationStrategies;
// FormValidator.js - 表单验证器
import validationStrategies from './validators/strategies';

class FormValidator {
  constructor(rules) {
    this.rules = rules;
    this.compiledRules = this.compileRules(rules);
  }

  // 编译验证规则
  compileRules(rules) {
    const compiled = {};

    Object.keys(rules).forEach(field => {
      const fieldRules = rules[field];
      compiled[field] = [];

      fieldRules.forEach(rule => {
        if (typeof rule === 'function') {
          // 直接使用函数
          compiled[field].push(rule);
        } else if (typeof rule === 'object') {
          // 使用策略配置
          const { type, ...options } = rule;
          const strategy = validationStrategies[type];

          if (!strategy) {
            console.warn(`Unknown validation strategy: ${type}`);
            return;
          }

          // 根据策略类型创建验证器
          if (type === 'required') {
            compiled[field].push((value) => strategy(value, options.message));
          } else {
            compiled[field].push(strategy(options.value, options.message));
          }
        }
      });
    });

    return compiled;
  }

  // 验证单个字段
  validateField(field, value) {
    const fieldRules = this.compiledRules[field];
    if (!fieldRules) return null;

    for (const rule of fieldRules) {
      const error = rule(value);
      if (error) return error;
    }

    return null;
  }

  // 验证整个表单
  validate(formData) {
    const errors = {};

    Object.keys(this.compiledRules).forEach(field => {
      const error = this.validateField(field, formData[field]);
      if (error) {
        errors[field] = error;
      }
    });

    return {
      isValid: Object.keys(errors).length === 0,
      errors,
    };
  }
}

export default FormValidator;
// LoginForm.jsx - 使用策略模式的表单
import React from 'react';
import FormValidator from './FormValidator';

function LoginForm() {
  const [formData, setFormData] = React.useState({
    email: '',
    password: '',
  });

  const [errors, setErrors] = React.useState({});

  // 定义验证规则(使用策略)
  const validator = React.useMemo(() => {
    return new FormValidator({
      email: [
        { type: 'required', message: '邮箱不能为空' },
        { type: 'email' },
      ],
      password: [
        { type: 'required', message: '密码不能为空' },
        { type: 'minLength', value: 6, message: '密码至少6个字符' },
        {
          type: 'custom',
          value: (value) => /[A-Z]/.test(value) && /[0-9]/.test(value),
          message: '密码必须包含大写字母和数字',
        },
      ],
    });
  }, []);

  const handleChange = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));

    // 实时验证
    const error = validator.validateField(field, value);
    setErrors(prev => ({ ...prev, [field]: error }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    const { isValid, errors: validationErrors } = validator.validate(formData);

    if (isValid) {
      console.log('提交表单:', formData);
    } else {
      setErrors(validationErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          placeholder="邮箱"
          value={formData.email}
          onChange={(e) => handleChange('email', e.target.value)}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <input
          type="password"
          placeholder="密码"
          value={formData.password}
          onChange={(e) => handleChange('password', e.target.value)}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <button type="submit">登录</button>
    </form>
  );
}

export default LoginForm;

效果

  • ✅ 易于扩展:添加新策略不修改核心代码
  • ✅ 代码清晰:每个策略独立
  • ✅ 易于测试:可以独立测试每个策略
  • ✅ 高度复用:策略可以在多个表单中复用

🏗️ 真实业务场景

场景1:支付方式策略
// payment/strategies.js - 支付策略
class PaymentStrategy {
  pay(amount) {
    throw new Error('Subclass must implement pay method');
  }

  refund(transactionId, amount) {
    throw new Error('Subclass must implement refund method');
  }
}

// 支付宝策略
class AlipayStrategy extends PaymentStrategy {
  async pay(amount) {
    console.log(`使用支付宝支付 ¥${amount}`);

    // 调用支付宝 SDK
    return {
      success: true,
      transactionId: 'ALIPAY-' + Date.now(),
      method: 'alipay',
    };
  }

  async refund(transactionId, amount) {
    console.log(`支付宝退款 ¥${amount}`);
    return { success: true };
  }
}

// 微信支付策略
class WechatStrategy extends PaymentStrategy {
  async pay(amount) {
    console.log(`使用微信支付 ¥${amount}`);

    // 调用微信支付 SDK
    return {
      success: true,
      transactionId: 'WECHAT-' + Date.now(),
      method: 'wechat',
    };
  }

  async refund(transactionId, amount) {
    console.log(`微信退款 ¥${amount}`);
    return { success: true };
  }
}

// 信用卡策略
class CreditCardStrategy extends PaymentStrategy {
  constructor(cardInfo) {
    super();
    this.cardInfo = cardInfo;
  }

  async pay(amount) {
    console.log(`使用信用卡支付 ¥${amount}`);
    console.log('卡号:', this.cardInfo.number);

    // 调用支付网关
    return {
      success: true,
      transactionId: 'CARD-' + Date.now(),
      method: 'credit_card',
    };
  }

  async refund(transactionId, amount) {
    console.log(`信用卡退款 ¥${amount}`);
    return { success: true };
  }
}

// 余额支付策略
class BalanceStrategy extends PaymentStrategy {
  constructor(userId) {
    super();
    this.userId = userId;
  }

  async pay(amount) {
    // 检查余额
    const balance = await this.getBalance();

    if (balance < amount) {
      throw new Error('余额不足');
    }

    console.log(`使用余额支付 ¥${amount}`);
    await this.deductBalance(amount);

    return {
      success: true,
      transactionId: 'BALANCE-' + Date.now(),
      method: 'balance',
    };
  }

  async refund(transactionId, amount) {
    console.log(`余额退款 ¥${amount}`);
    await this.addBalance(amount);
    return { success: true };
  }

  async getBalance() {
    // 获取用户余额
    return 1000;
  }

  async deductBalance(amount) {
    // 扣除余额
  }

  async addBalance(amount) {
    // 增加余额
  }
}

export {
  PaymentStrategy,
  AlipayStrategy,
  WechatStrategy,
  CreditCardStrategy,
  BalanceStrategy,
};
// PaymentManager.js - 支付管理器
class PaymentManager {
  constructor() {
    this.strategy = null;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  async executePayment(amount) {
    if (!this.strategy) {
      throw new Error('Payment strategy not set');
    }

    try {
      const result = await this.strategy.pay(amount);
      console.log('支付成功:', result);
      return result;
    } catch (error) {
      console.error('支付失败:', error);
      throw error;
    }
  }

  async executeRefund(transactionId, amount) {
    if (!this.strategy) {
      throw new Error('Payment strategy not set');
    }

    try {
      const result = await this.strategy.refund(transactionId, amount);
      console.log('退款成功:', result);
      return result;
    } catch (error) {
      console.error('退款失败:', error);
      throw error;
    }
  }
}

export default PaymentManager;
// CheckoutPage.jsx - 结账页面
import React from 'react';
import PaymentManager from './PaymentManager';
import {
  AlipayStrategy,
  WechatStrategy,
  CreditCardStrategy,
  BalanceStrategy,
} from './payment/strategies';

function CheckoutPage({ totalAmount }) {
  const [paymentMethod, setPaymentMethod] = React.useState('alipay');
  const [paying, setPaying] = React.useState(false);

  const paymentManager = React.useMemo(() => new PaymentManager(), []);

  const handlePay = async () => {
    setPaying(true);

    try {
      // 根据选择的支付方式设置策略
      switch (paymentMethod) {
        case 'alipay':
          paymentManager.setStrategy(new AlipayStrategy());
          break;
        case 'wechat':
          paymentManager.setStrategy(new WechatStrategy());
          break;
        case 'credit_card':
          paymentManager.setStrategy(new CreditCardStrategy({
            number: '4111111111111111',
            cvv: '123',
            expiry: '12/25',
          }));
          break;
        case 'balance':
          paymentManager.setStrategy(new BalanceStrategy('user123'));
          break;
        default:
          throw new Error('Invalid payment method');
      }

      // 执行支付(不关心具体实现)
      const result = await paymentManager.executePayment(totalAmount);
      alert('支付成功!');
    } catch (error) {
      alert('支付失败:' + error.message);
    } finally {
      setPaying(false);
    }
  };

  return (
    <div className="checkout-page">
      <h2>支付 ¥{totalAmount}</h2>

      <div className="payment-methods">
        <label>
          <input
            type="radio"
            value="alipay"
            checked={paymentMethod === 'alipay'}
            onChange={(e) => setPaymentMethod(e.target.value)}
          />
          支付宝
        </label>

        <label>
          <input
            type="radio"
            value="wechat"
            checked={paymentMethod === 'wechat'}
            onChange={(e) => setPaymentMethod(e.target.value)}
          />
          微信支付
        </label>

        <label>
          <input
            type="radio"
            value="credit_card"
            checked={paymentMethod === 'credit_card'}
            onChange={(e) => setPaymentMethod(e.target.value)}
          />
          信用卡
        </label>

        <label>
          <input
            type="radio"
            value="balance"
            checked={paymentMethod === 'balance'}
            onChange={(e) => setPaymentMethod(e.target.value)}
          />
          余额支付
        </label>
      </div>

      <button onClick={handlePay} disabled={paying}>
        {paying ? '支付中...' : '确认支付'}
      </button>
    </div>
  );
}

export default CheckoutPage;
场景2:排序策略
// sorting/strategies.js - 排序策略
class SortStrategy {
  sort(items) {
    throw new Error('Subclass must implement sort method');
  }
}

// 价格升序
class PriceAscStrategy extends SortStrategy {
  sort(items) {
    return [...items].sort((a, b) => a.price - b.price);
  }
}

// 价格降序
class PriceDescStrategy extends SortStrategy {
  sort(items) {
    return [...items].sort((a, b) => b.price - a.price);
  }
}

// 按评分排序
class RatingStrategy extends SortStrategy {
  sort(items) {
    return [...items].sort((a, b) => b.rating - a.rating);
  }
}

// 按销量排序
class SalesStrategy extends SortStrategy {
  sort(items) {
    return [...items].sort((a, b) => b.sales - a.sales);
  }
}

// 按最新排序
class NewestStrategy extends SortStrategy {
  sort(items) {
    return [...items].sort((a, b) =>
      new Date(b.createdAt) - new Date(a.createdAt)
    );
  }
}

export {
  PriceAscStrategy,
  PriceDescStrategy,
  RatingStrategy,
  SalesStrategy,
  NewestStrategy,
};
// ProductList.jsx - 商品列表
import React from 'react';
import {
  PriceAscStrategy,
  PriceDescStrategy,
  RatingStrategy,
  SalesStrategy,
  NewestStrategy,
} from './sorting/strategies';

function ProductList({ products }) {
  const [sortBy, setSortBy] = React.useState('price_asc');

  const sortStrategies = {
    price_asc: new PriceAscStrategy(),
    price_desc: new PriceDescStrategy(),
    rating: new RatingStrategy(),
    sales: new SalesStrategy(),
    newest: new NewestStrategy(),
  };

  const sortedProducts = React.useMemo(() => {
    const strategy = sortStrategies[sortBy];
    return strategy.sort(products);
  }, [products, sortBy]);

  return (
    <div className="product-list">
      <div className="sort-options">
        <label>排序方式:</label>
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="price_asc">价格从低到高</option>
          <option value="price_desc">价格从高到低</option>
          <option value="rating">按评分</option>
          <option value="sales">按销量</option>
          <option value="newest">最新上架</option>
        </select>
      </div>

      <div className="products">
        {sortedProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

function ProductCard({ product }) {
  return (
    <div className="product-card">
      <h4>{product.name}</h4>
      <p>价格: ¥{product.price}</p>
      <p>评分: {product.rating}⭐</p>
      <p>销量: {product.sales}</p>
    </div>
  );
}

export default ProductList;

🎨 在主流框架中的应用

React Hooks - 策略模式的应用
// useAuth hook 使用不同的认证策略
function useAuth(strategy = 'jwt') {
  const strategies = {
    jwt: useJWTAuth,
    oauth: useOAuthAuth,
    session: useSessionAuth,
  };

  return strategies[strategy]();
}

function useJWTAuth() {
  // JWT 认证逻辑
}

function useOAuthAuth() {
  // OAuth 认证逻辑
}

function useSessionAuth() {
  // Session 认证逻辑
}
Passport.js - 认证策略
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// 使用本地策略
passport.use(new LocalStrategy(
  function(username, password, done) {
    // 验证逻辑
  }
));

// 使用 Google 策略
passport.use(new GoogleStrategy({
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: "http://localhost:3000/auth/google/callback"
  },
  function(accessToken, refreshToken, profile, cb) {
    // 验证逻辑
  }
));

⚖️ 优缺点分析

✅ 优点
  1. 开闭原则:添加新策略不需要修改现有代码
  2. 消除条件语句:避免大量 if-else
  3. 算法独立:每个策略独立,易于测试
  4. 运行时切换:可以在运行时切换策略
❌ 缺点
  1. 类数量增加:每个策略都是一个类
  2. 客户端必须了解策略:需要知道有哪些策略可选
  3. 策略间通信:策略之间很难共享数据

📋 何时使用策略模式

✅ 适合使用的场景
  • 表单验证规则
  • 支付方式、物流方式
  • 排序算法、搜索算法
  • 数据格式化、数据转换
  • 认证方式、加密算法
❌ 不适合使用的场景
  • 只有一两种算法
  • 算法很少变化
  • 策略之间需要共享大量数据

命令模式

💡 模式定义

命令模式(Command Pattern)将请求封装成对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

🤔 为什么需要命令模式?

问题场景:撤销重做、操作历史、宏命令

假设你在开发一个富文本编辑器,需要支持撤销/重做功能:

❌ 不使用命令模式的痛点
// TextEditor.jsx - 难以实现撤销重做
function TextEditor() {
  const [content, setContent] = React.useState('');
  const [history, setHistory] = React.useState([]);
  const [historyIndex, setHistoryIndex] = React.useState(-1);

  const handleInsertText = (text, position) => {
    const newContent =
      content.slice(0, position) + text + content.slice(position);
    setContent(newContent);

    // 如何保存足够的信息来撤销?
    // 需要记录:操作类型、位置、文本、旧内容...
    setHistory([...history.slice(0, historyIndex + 1), {
      type: 'insert',
      text,
      position,
      oldContent: content,
    }]);
    setHistoryIndex(historyIndex + 1);
  };

  const handleDeleteText = (start, end) => {
    const deletedText = content.slice(start, end);
    const newContent = content.slice(0, start) + content.slice(end);
    setContent(newContent);

    setHistory([...history.slice(0, historyIndex + 1), {
      type: 'delete',
      start,
      end,
      deletedText,
      oldContent: content,
    }]);
    setHistoryIndex(historyIndex + 1);
  };

  const handleBold = (start, end) => {
    // 加粗操作...
    // 又要记录不同的历史信息
  };

  const handleUndo = () => {
    if (historyIndex < 0) return;

    const action = history[historyIndex];

    // 根据不同操作类型,执行不同的撤销逻辑
    if (action.type === 'insert') {
      setContent(action.oldContent);
    } else if (action.type === 'delete') {
      const newContent =
        content.slice(0, action.start) +
        action.deletedText +
        content.slice(action.start);
      setContent(newContent);
    } else if (action.type === 'bold') {
      // 撤销加粗...
    }
    // 每种操作都要写撤销逻辑!

    setHistoryIndex(historyIndex - 1);
  };

  const handleRedo = () => {
    // 重做也要写一遍逻辑...
  };

  // ...
}

问题

  • 撤销逻辑复杂:每种操作都要写撤销/重做逻辑
  • 难以扩展:新增操作需要修改撤销/重做代码
  • 代码重复:相似的历史记录逻辑
  • 难以测试:操作和撤销耦合在一起

✅ 使用命令模式解决
// commands/Command.js - 命令基类
class Command {
  execute() {
    throw new Error('Subclass must implement execute method');
  }

  undo() {
    throw new Error('Subclass must implement undo method');
  }

  redo() {
    return this.execute();
  }
}

export default Command;
// commands/InsertTextCommand.js - 插入文本命令
import Command from './Command';

class InsertTextCommand extends Command {
  constructor(editor, text, position) {
    super();
    this.editor = editor;
    this.text = text;
    this.position = position;
    this.oldContent = null;
  }

  execute() {
    // 保存旧内容
    this.oldContent = this.editor.getContent();

    // 执行插入
    const content = this.editor.getContent();
    const newContent =
      content.slice(0, this.position) +
      this.text +
      content.slice(this.position);

    this.editor.setContent(newContent);
    return true;
  }

  undo() {
    // 恢复旧内容
    this.editor.setContent(this.oldContent);
  }
}

export default InsertTextCommand;
// commands/DeleteTextCommand.js - 删除文本命令
import Command from './Command';

class DeleteTextCommand extends Command {
  constructor(editor, start, end) {
    super();
    this.editor = editor;
    this.start = start;
    this.end = end;
    this.deletedText = null;
  }

  execute() {
    const content = this.editor.getContent();

    // 保存被删除的文本
    this.deletedText = content.slice(this.start, this.end);

    // 执行删除
    const newContent = content.slice(0, this.start) + content.slice(this.end);
    this.editor.setContent(newContent);
    return true;
  }

  undo() {
    // 恢复被删除的文本
    const content = this.editor.getContent();
    const newContent =
      content.slice(0, this.start) +
      this.deletedText +
      content.slice(this.start);

    this.editor.setContent(newContent);
  }
}

export default DeleteTextCommand;
// commands/BoldTextCommand.js - 加粗文本命令
import Command from './Command';

class BoldTextCommand extends Command {
  constructor(editor, start, end) {
    super();
    this.editor = editor;
    this.start = start;
    this.end = end;
    this.oldFormat = null;
  }

  execute() {
    // 保存旧格式
    this.oldFormat = this.editor.getFormat(this.start, this.end);

    // 应用加粗
    this.editor.applyFormat(this.start, this.end, { bold: true });
    return true;
  }

  undo() {
    // 恢复旧格式
    this.editor.applyFormat(this.start, this.end, this.oldFormat);
  }
}

export default BoldTextCommand;
// CommandManager.js - 命令管理器
class CommandManager {
  constructor() {
    this.history = [];
    this.currentIndex = -1;
  }

  // 执行命令
  execute(command) {
    // 清除当前位置之后的历史
    this.history = this.history.slice(0, this.currentIndex + 1);

    // 执行命令
    const success = command.execute();

    if (success) {
      // 添加到历史
      this.history.push(command);
      this.currentIndex++;
    }

    return success;
  }

  // 撤销
  undo() {
    if (!this.canUndo()) return false;

    const command = this.history[this.currentIndex];
    command.undo();
    this.currentIndex--;

    return true;
  }

  // 重做
  redo() {
    if (!this.canRedo()) return false;

    this.currentIndex++;
    const command = this.history[this.currentIndex];
    command.redo();

    return true;
  }

  canUndo() {
    return this.currentIndex >= 0;
  }

  canRedo() {
    return this.currentIndex < this.history.length - 1;
  }

  getHistory() {
    return this.history.map((cmd, index) => ({
      name: cmd.constructor.name,
      active: index <= this.currentIndex,
    }));
  }

  clear() {
    this.history = [];
    this.currentIndex = -1;
  }
}

export default CommandManager;
// TextEditor.jsx - 使用命令模式的编辑器
import React from 'react';
import CommandManager from './CommandManager';
import InsertTextCommand from './commands/InsertTextCommand';
import DeleteTextCommand from './commands/DeleteTextCommand';
import BoldTextCommand from './commands/BoldTextCommand';

function TextEditor() {
  const [content, setContent] = React.useState('');
  const [selection, setSelection] = React.useState({ start: 0, end: 0 });
  const [formats, setFormats] = React.useState({});

  const commandManager = React.useRef(new CommandManager());

  // 编辑器接口
  const editor = React.useMemo(() => ({
    getContent: () => content,
    setContent,
    getFormat: (start, end) => formats[`${start}-${end}`] || {},
    applyFormat: (start, end, format) => {
      setFormats(prev => ({
        ...prev,
        [`${start}-${end}`]: format,
      }));
    },
  }), [content, formats]);

  const handleInsert = (text) => {
    const command = new InsertTextCommand(editor, text, selection.start);
    commandManager.current.execute(command);
  };

  const handleDelete = () => {
    if (selection.start === selection.end) return;
    const command = new DeleteTextCommand(editor, selection.start, selection.end);
    commandManager.current.execute(command);
  };

  const handleBold = () => {
    if (selection.start === selection.end) return;
    const command = new BoldTextCommand(editor, selection.start, selection.end);
    commandManager.current.execute(command);
  };

  const handleUndo = () => {
    commandManager.current.undo();
  };

  const handleRedo = () => {
    commandManager.current.redo();
  };

  return (
    <div className="text-editor">
      <div className="toolbar">
        <button onClick={() => handleInsert('Hello')}>插入 Hello</button>
        <button onClick={handleDelete}>删除选中</button>
        <button onClick={handleBold}>加粗</button>
        <button
          onClick={handleUndo}
          disabled={!commandManager.current.canUndo()}
        >
          撤销
        </button>
        <button
          onClick={handleRedo}
          disabled={!commandManager.current.canRedo()}
        >
          重做
        </button>
      </div>

      <textarea
        value={content}
        onChange={(e) => {
          const command = new InsertTextCommand(
            editor,
            e.target.value.slice(content.length),
            content.length
          );
          commandManager.current.execute(command);
        }}
        onSelect={(e) => {
          setSelection({
            start: e.target.selectionStart,
            end: e.target.selectionEnd,
          });
        }}
      />

      <HistoryPanel commandManager={commandManager.current} />
    </div>
  );
}

function HistoryPanel({ commandManager }) {
  const [history, setHistory] = React.useState([]);

  React.useEffect(() => {
    const interval = setInterval(() => {
      setHistory(commandManager.getHistory());
    }, 100);

    return () => clearInterval(interval);
  }, [commandManager]);

  return (
    <div className="history-panel">
      <h4>操作历史</h4>
      <ul>
        {history.map((item, index) => (
          <li key={index} className={item.active ? 'active' : 'inactive'}>
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TextEditor;

效果

  • ✅ 统一接口:所有命令都有 execute/undo/redo 方法
  • ✅ 易于扩展:新增命令不影响现有代码
  • ✅ 操作历史:自动记录所有操作
  • ✅ 易于测试:可以独立测试每个命令

🏗️ 真实业务场景

场景1:图形编辑器(移动、缩放、旋转)
// commands/MoveCommand.js
class MoveCommand extends Command {
  constructor(shape, deltaX, deltaY) {
    super();
    this.shape = shape;
    this.deltaX = deltaX;
    this.deltaY = deltaY;
  }

  execute() {
    this.shape.x += this.deltaX;
    this.shape.y += this.deltaY;
  }

  undo() {
    this.shape.x -= this.deltaX;
    this.shape.y -= this.deltaY;
  }
}

// commands/ResizeCommand.js
class ResizeCommand extends Command {
  constructor(shape, newWidth, newHeight) {
    super();
    this.shape = shape;
    this.newWidth = newWidth;
    this.newHeight = newHeight;
    this.oldWidth = shape.width;
    this.oldHeight = shape.height;
  }

  execute() {
    this.shape.width = this.newWidth;
    this.shape.height = this.newHeight;
  }

  undo() {
    this.shape.width = this.oldWidth;
    this.shape.height = this.oldHeight;
  }
}

// commands/MacroCommand.js - 宏命令(组合多个命令)
class MacroCommand extends Command {
  constructor(commands = []) {
    super();
    this.commands = commands;
  }

  add(command) {
    this.commands.push(command);
  }

  execute() {
    this.commands.forEach(command => command.execute());
  }

  undo() {
    // 逆序撤销
    for (let i = this.commands.length - 1; i >= 0; i--) {
      this.commands[i].undo();
    }
  }
}
场景2:任务队列
// TaskQueue.js - 任务队列
class TaskQueue {
  constructor() {
    this.queue = [];
    this.history = [];
    this.isProcessing = false;
  }

  // 添加任务
  enqueue(command) {
    this.queue.push(command);
    this.processNext();
  }

  // 处理下一个任务
  async processNext() {
    if (this.isProcessing || this.queue.length === 0) return;

    this.isProcessing = true;
    const command = this.queue.shift();

    try {
      await command.execute();
      this.history.push(command);
      console.log(`任务完成: ${command.constructor.name}`);
    } catch (error) {
      console.error(`任务失败: ${command.constructor.name}`, error);
      // 可以选择重试或跳过
    } finally {
      this.isProcessing = false;
      this.processNext();
    }
  }

  // 取消所有任务
  cancelAll() {
    this.queue = [];
  }

  // 重试失败的任务
  retryLast() {
    if (this.history.length === 0) return;
    const lastCommand = this.history[this.history.length - 1];
    this.enqueue(lastCommand);
  }
}

// 使用示例
const taskQueue = new TaskQueue();

class UploadFileCommand extends Command {
  constructor(file) {
    super();
    this.file = file;
  }

  async execute() {
    console.log(`上传文件: ${this.file.name}`);
    // 模拟上传
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

// 批量上传
files.forEach(file => {
  taskQueue.enqueue(new UploadFileCommand(file));
});

🎨 在主流框架中的应用

Redux Action - 命令模式
// Redux 的 Action 就是命令模式
const incrementAction = { type: 'INCREMENT' };
const decrementAction = { type: 'DECREMENT' };

// Dispatch 就是执行命令
dispatch(incrementAction);

// Redux DevTools 支持时间旅行(撤销/重做)
Git 命令 - 命令模式的典范
git add .           # 添加到暂存区(可撤销)
git commit -m "..."  # 提交(可撤销)
git revert HEAD     # 撤销最后一次提交
git reset --hard    # 重置到某个状态

⚖️ 优缺点分析

✅ 优点
  1. 解耦:调用者和接收者解耦
  2. 可撤销:轻松实现撤销/重做
  3. 可组合:可以组合多个命令(宏命令)
  4. 可记录:可以记录操作历史
  5. 可排队:可以实现任务队列
❌ 缺点
  1. 类数量增加:每个操作都是一个命令类
  2. 内存开销:需要保存历史状态
  3. 复杂度:简单操作也要封装成命令

📋 何时使用命令模式

✅ 适合使用的场景
  • 撤销/重做功能
  • 操作历史记录
  • 任务队列、批处理
  • 事务管理
  • 宏命令(组合多个操作)
❌ 不适合使用的场景
  • 简单的 CRUD 操作
  • 不需要撤销的操作
  • 内存敏感的场景

状态模式

💡 模式定义

状态模式(State Pattern)允许对象在内部状态改变时改变其行为,对象看起来好像修改了它的类。

🤔 为什么需要状态模式?

问题场景:订单状态、播放器状态、流程控制

假设你在开发一个音乐播放器:

❌ 不使用状态模式的痛点
// MusicPlayer.jsx - 充满状态判断
function MusicPlayer({ track }) {
  const [state, setState] = React.useState('stopped'); // stopped, playing, paused
  const audioRef = React.useRef(new Audio(track.url));

  const handlePlay = () => {
    // 根据当前状态判断能否播放
    if (state === 'stopped') {
      audioRef.current.play();
      setState('playing');
    } else if (state === 'paused') {
      audioRef.current.play();
      setState('playing');
    } else if (state === 'playing') {
      // 已经在播放,什么都不做
      console.log('Already playing');
    }
  };

  const handlePause = () => {
    if (state === 'playing') {
      audioRef.current.pause();
      setState('paused');
    } else if (state === 'stopped') {
      // 停止状态不能暂停
      console.log('Cannot pause when stopped');
    } else if (state === 'paused') {
      // 已经暂停
      console.log('Already paused');
    }
  };

  const handleStop = () => {
    if (state === 'playing' || state === 'paused') {
      audioRef.current.pause();
      audioRef.current.currentTime = 0;
      setState('stopped');
    } else if (state === 'stopped') {
      console.log('Already stopped');
    }
  };

  const handleNext = () => {
    if (state === 'playing') {
      // 播放中可以切换
      audioRef.current.pause();
      // 加载下一首
      setState('playing');
    } else if (state === 'paused') {
      // 暂停时也可以切换
      // 加载下一首
      setState('paused');
    } else if (state === 'stopped') {
      // 停止时切换但不播放
      // 加载下一首
      setState('stopped');
    }
  };

  // 每个操作都要判断所有可能的状态!
  // 新增状态(如 buffering, error)会导致大量修改

  return (
    <div className="music-player">
      <p>状态: {state}</p>
      <button onClick={handlePlay}>播放</button>
      <button onClick={handlePause}>暂停</button>
      <button onClick={handleStop}>停止</button>
      <button onClick={handleNext}>下一首</button>
    </div>
  );
}

问题

  • 状态判断分散:每个方法都要判断所有状态
  • 难以扩展:新增状态需要修改所有方法
  • 代码重复:相似的状态判断逻辑
  • 状态转换不清晰:不知道哪些状态可以互相转换

✅ 使用状态模式解决
// states/PlayerState.js - 状态基类
class PlayerState {
  constructor(player) {
    this.player = player;
  }

  play() {
    console.log('Cannot play in this state');
  }

  pause() {
    console.log('Cannot pause in this state');
  }

  stop() {
    console.log('Cannot stop in this state');
  }

  next() {
    console.log('Cannot switch track in this state');
  }

  getName() {
    return this.constructor.name;
  }
}

export default PlayerState;
// states/StoppedState.js
import PlayerState from './PlayerState';

class StoppedState extends PlayerState {
  play() {
    console.log('Starting playback...');
    this.player.audio.play();
    this.player.setState(this.player.states.playing);
  }

  pause() {
    console.log('Cannot pause when stopped');
  }

  stop() {
    console.log('Already stopped');
  }

  next() {
    console.log('Switching to next track (stopped)');
    this.player.loadNextTrack();
    // 保持停止状态
  }
}

export default StoppedState;
// states/PlayingState.js
import PlayerState from './PlayerState';

class PlayingState extends PlayerState {
  play() {
    console.log('Already playing');
  }

  pause() {
    console.log('Pausing playback...');
    this.player.audio.pause();
    this.player.setState(this.player.states.paused);
  }

  stop() {
    console.log('Stopping playback...');
    this.player.audio.pause();
    this.player.audio.currentTime = 0;
    this.player.setState(this.player.states.stopped);
  }

  next() {
    console.log('Switching to next track (playing)');
    this.player.loadNextTrack();
    this.player.audio.play();
    // 保持播放状态
  }
}

export default PlayingState;
// states/PausedState.js
import PlayerState from './PlayerState';

class PausedState extends PlayerState {
  play() {
    console.log('Resuming playback...');
    this.player.audio.play();
    this.player.setState(this.player.states.playing);
  }

  pause() {
    console.log('Already paused');
  }

  stop() {
    console.log('Stopping playback...');
    this.player.audio.currentTime = 0;
    this.player.setState(this.player.states.stopped);
  }

  next() {
    console.log('Switching to next track (paused)');
    this.player.loadNextTrack();
    // 保持暂停状态
  }
}

export default PausedState;
// MusicPlayer.js - 播放器类
import StoppedState from './states/StoppedState';
import PlayingState from './states/PlayingState';
import PausedState from './states/PausedState';

class MusicPlayer {
  constructor(track) {
    this.audio = new Audio(track.url);
    this.currentTrack = track;

    // 创建所有状态实例
    this.states = {
      stopped: new StoppedState(this),
      playing: new PlayingState(this),
      paused: new PausedState(this),
    };

    // 初始状态
    this.currentState = this.states.stopped;
    this.listeners = [];
  }

  // 设置状态
  setState(state) {
    console.log(`State: ${this.currentState.getName()} -> ${state.getName()}`);
    this.currentState = state;
    this.notifyListeners();
  }

  // 操作方法(委托给当前状态)
  play() {
    this.currentState.play();
  }

  pause() {
    this.currentState.pause();
  }

  stop() {
    this.currentState.stop();
  }

  next() {
    this.currentState.next();
  }

  loadNextTrack() {
    // 加载下一首歌
    console.log('Loading next track...');
  }

  getState() {
    return this.currentState.getName();
  }

  // 观察者模式:通知状态变化
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  notifyListeners() {
    this.listeners.forEach(listener => listener(this.getState()));
  }
}

export default MusicPlayer;
// MusicPlayerUI.jsx - React 组件
import React from 'react';
import MusicPlayer from './MusicPlayer';

function MusicPlayerUI({ track }) {
  const [state, setState] = React.useState('StoppedState');
  const playerRef = React.useRef(null);

  React.useEffect(() => {
    const player = new MusicPlayer(track);
    playerRef.current = player;

    // 订阅状态变化
    const unsubscribe = player.subscribe(setState);

    return () => {
      unsubscribe();
    };
  }, [track]);

  const handlePlay = () => playerRef.current?.play();
  const handlePause = () => playerRef.current?.pause();
  const handleStop = () => playerRef.current?.stop();
  const handleNext = () => playerRef.current?.next();

  return (
    <div className="music-player">
      <p>当前状态: {state}</p>
      <div className="controls">
        <button onClick={handlePlay}>▶️ 播放</button>
        <button onClick={handlePause}>⏸️ 暂停</button>
        <button onClick={handleStop}>⏹️ 停止</button>
        <button onClick={handleNext}>⏭️ 下一首</button>
      </div>
    </div>
  );
}

export default MusicPlayerUI;

效果

  • ✅ 状态逻辑集中:每个状态类只处理自己的逻辑
  • ✅ 易于扩展:新增状态只需添加新类
  • ✅ 状态转换清晰:一目了然
  • ✅ 消除条件语句:没有 if-else

🏗️ 真实业务场景

场景1:订单状态机
// states/OrderState.js
class OrderState {
  constructor(order) {
    this.order = order;
  }

  pay() {
    throw new Error('Cannot pay in this state');
  }

  ship() {
    throw new Error('Cannot ship in this state');
  }

  deliver() {
    throw new Error('Cannot deliver in this state');
  }

  cancel() {
    throw new Error('Cannot cancel in this state');
  }

  getName() {
    return this.constructor.name;
  }
}

// 待支付状态
class PendingPaymentState extends OrderState {
  pay() {
    console.log('Processing payment...');
    // 支付逻辑
    this.order.setState(this.order.states.paid);
  }

  cancel() {
    console.log('Cancelling order...');
    this.order.setState(this.order.states.cancelled);
  }
}

// 已支付状态
class PaidState extends OrderState {
  ship() {
    console.log('Shipping order...');
    this.order.setState(this.order.states.shipped);
  }

  cancel() {
    console.log('Cancelling and refunding...');
    // 退款逻辑
    this.order.setState(this.order.states.cancelled);
  }
}

// 已发货状态
class ShippedState extends OrderState {
  deliver() {
    console.log('Order delivered!');
    this.order.setState(this.order.states.delivered);
  }
}

// 已完成状态
class DeliveredState extends OrderState {
  // 已完成,不能再操作
}

// 已取消状态
class CancelledState extends OrderState {
  // 已取消,不能再操作
}

// Order.js
class Order {
  constructor(orderId) {
    this.orderId = orderId;

    this.states = {
      pendingPayment: new PendingPaymentState(this),
      paid: new PaidState(this),
      shipped: new ShippedState(this),
      delivered: new DeliveredState(this),
      cancelled: new CancelledState(this),
    };

    this.currentState = this.states.pendingPayment;
  }

  setState(state) {
    console.log(`Order ${this.orderId}: ${this.currentState.getName()} -> ${state.getName()}`);
    this.currentState = state;
  }

  pay() {
    this.currentState.pay();
  }

  ship() {
    this.currentState.ship();
  }

  deliver() {
    this.currentState.deliver();
  }

  cancel() {
    this.currentState.cancel();
  }

  getStatus() {
    return this.currentState.getName();
  }
}

export default Order;
// 使用订单状态机
const order = new Order('ORDER-123');

console.log(order.getStatus()); // PendingPaymentState

order.pay(); // 支付成功 -> PaidState
order.ship(); // 发货 -> ShippedState
order.deliver(); // 送达 -> DeliveredState

// order.cancel(); // 报错:Cannot cancel in this state
场景2:TCP 连接状态
// Connection.js - TCP 连接状态机
class ConnectionState {
  constructor(connection) {
    this.connection = connection;
  }

  open() {}
  close() {}
  send(data) {}
  receive(data) {}
}

class ClosedState extends ConnectionState {
  open() {
    console.log('Opening connection...');
    this.connection.socket.connect();
    this.connection.setState(this.connection.states.connecting);
  }

  close() {
    console.log('Already closed');
  }

  send(data) {
    throw new Error('Cannot send data when connection is closed');
  }
}

class ConnectingState extends ConnectionState {
  open() {
    console.log('Already connecting');
  }

  close() {
    console.log('Aborting connection...');
    this.connection.socket.abort();
    this.connection.setState(this.connection.states.closed);
  }

  send(data) {
    throw new Error('Cannot send data while connecting');
  }
}

class ConnectedState extends ConnectionState {
  open() {
    console.log('Already connected');
  }

  close() {
    console.log('Closing connection...');
    this.connection.socket.close();
    this.connection.setState(this.connection.states.closed);
  }

  send(data) {
    console.log('Sending data:', data);
    this.connection.socket.send(data);
  }

  receive(data) {
    console.log('Received data:', data);
    this.connection.onData(data);
  }
}

class Connection {
  constructor() {
    this.socket = null;

    this.states = {
      closed: new ClosedState(this),
      connecting: new ConnectingState(this),
      connected: new ConnectedState(this),
    };

    this.currentState = this.states.closed;
  }

  setState(state) {
    this.currentState = state;
  }

  open() {
    this.currentState.open();
  }

  close() {
    this.currentState.close();
  }

  send(data) {
    this.currentState.send(data);
  }

  receive(data) {
    this.currentState.receive(data);
  }

  onData(data) {
    // 处理接收到的数据
  }
}

🎨 在主流框架中的应用

Promise 状态
// Promise 有三个状态:pending, fulfilled, rejected
const promise = new Promise((resolve, reject) => {
  // pending 状态
  setTimeout(() => {
    resolve('success'); // 转换到 fulfilled 状态
  }, 1000);
});

promise.then(
  value => console.log(value), // fulfilled 状态
  error => console.error(error) // rejected 状态
);
React 组件生命周期
class MyComponent extends React.Component {
  // Mounting 状态
  componentDidMount() {}

  // Updating 状态
  componentDidUpdate() {}

  // Unmounting 状态
  componentWillUnmount() {}
}

⚖️ 优缺点分析

✅ 优点
  1. 消除条件语句:避免大量 if-else
  2. 单一职责:每个状态类只处理一个状态
  3. 状态转换清晰:一目了然
  4. 易于扩展:添加新状态不影响现有状态
❌ 缺点
  1. 类数量增加:每个状态都是一个类
  2. 状态过多时复杂:状态很多时难以管理
  3. 状态共享困难:状态之间共享数据需要通过上下文对象

📋 何时使用状态模式

✅ 适合使用的场景
  • 对象行为随状态改变而改变
  • 大量条件语句(if-else, switch)
  • 状态转换规则明确
  • 状态机、工作流
❌ 不适合使用的场景
  • 只有 2-3 个状态
  • 状态转换简单
  • 状态之间有大量共享逻辑

📝 总结

行为型模式对比

模式核心目的使用场景优先级
观察者模式对象间一对多依赖事件系统、状态订阅⭐⭐⭐⭐⭐
策略模式算法可替换表单验证、支付方式⭐⭐⭐⭐⭐
命令模式请求封装成对象撤销重做、任务队列⭐⭐⭐⭐
状态模式状态改变行为状态机、流程控制⭐⭐⭐⭐

学习建议

  1. 观察者模式:前端最常用,掌握 EventEmitter 和 React Context
  2. 策略模式:理解如何消除 if-else,提升代码质量
  3. 命令模式:理解撤销/重做的实现原理
  4. 状态模式:学会用状态机处理复杂流程

模式组合使用

在实际开发中,这些模式经常组合使用:

  • 观察者 + 命令:命令执行后通知观察者
  • 策略 + 工厂:使用工厂创建策略对象
  • 状态 + 命令:状态转换通过命令实现