设计模式-策略模式

前言

在日常的前端开发中,我们经常会遇到这样的场景:根据不同的条件执行不同的逻辑。最常见的做法是使用大量的 if-elseswitch 语句,但这样的代码往往难以维护和扩展。今天我们来深入探讨一个优雅的解决方案------策略模式(Strategy Pattern)。

🎯 什么是策略模式

策略模式是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。

核心概念

  • Strategy(策略):定义所有具体策略的公共接口
  • ConcreteStrategy(具体策略):实现具体的算法
  • Context(上下文):持有策略引用,并将工作委托给策略对象

传统问题示例

javascript 复制代码
// ❌ 传统的条件判断方式
function calculatePrice(price, discountType) {
  if (discountType === 'student') {
    return price * 0.8;
  } else if (discountType === 'vip') {
    return price * 0.7;
  } else if (discountType === 'member') {
    return price * 0.9;
  } else if (discountType === 'newuser') {
    return price * 0.85;
  } else {
    return price;
  }
}

问题

  1. 代码冗长,难以维护
  2. 违反开闭原则(对扩展开放,对修改封闭)
  3. 新增折扣类型需要修改现有代码
  4. 难以进行单元测试

🛠️ 策略模式的基础实现

TypeScript 版本

typescript 复制代码
// 策略接口
interface DiscountStrategy {
  calculate(price: number): number;
  getDescription(): string;
}

// 具体策略实现
class StudentDiscountStrategy implements DiscountStrategy {
  calculate(price: number): number {
    return price * 0.8;
  }
  
  getDescription(): string {
    return '学生折扣 8折';
  }
}

class VipDiscountStrategy implements DiscountStrategy {
  calculate(price: number): number {
    return price * 0.7;
  }
  
  getDescription(): string {
    return 'VIP折扣 7折';
  }
}

class MemberDiscountStrategy implements DiscountStrategy {
  calculate(price: number): number {
    return price * 0.9;
  }
  
  getDescription(): string {
    return '会员折扣 9折';
  }
}

class NewUserDiscountStrategy implements DiscountStrategy {
  calculate(price: number): number {
    return price * 0.85;
  }
  
  getDescription(): string {
    return '新用户折扣 8.5折';
  }
}

class NoDiscountStrategy implements DiscountStrategy {
  calculate(price: number): number {
    return price;
  }
  
  getDescription(): string {
    return '无折扣';
  }
}

// 上下文类
class PriceCalculator {
  private strategy: DiscountStrategy;
  
  constructor(strategy: DiscountStrategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: DiscountStrategy): void {
    this.strategy = strategy;
  }
  
  calculatePrice(originalPrice: number): number {
    return this.strategy.calculate(originalPrice);
  }
  
  getDiscountDescription(): string {
    return this.strategy.getDescription();
  }
}

// 使用示例
const calculator = new PriceCalculator(new NoDiscountStrategy());

// 计算学生折扣
calculator.setStrategy(new StudentDiscountStrategy());
const studentPrice = calculator.calculatePrice(100); // 80
console.log(`${calculator.getDiscountDescription()}: ¥${studentPrice}`);

// 计算VIP折扣
calculator.setStrategy(new VipDiscountStrategy());
const vipPrice = calculator.calculatePrice(100); // 70
console.log(`${calculator.getDiscountDescription()}: ¥${vipPrice}`);

工厂模式结合策略模式

typescript 复制代码
// 策略工厂
class DiscountStrategyFactory {
  private static strategies = new Map<string, DiscountStrategy>([
    ['student', new StudentDiscountStrategy()],
    ['vip', new VipDiscountStrategy()],
    ['member', new MemberDiscountStrategy()],
    ['newuser', new NewUserDiscountStrategy()],
    ['none', new NoDiscountStrategy()]
  ]);
  
  static getStrategy(type: string): DiscountStrategy {
    const strategy = this.strategies.get(type);
    if (!strategy) {
      throw new Error(`Unknown discount type: ${type}`);
    }
    return strategy;
  }
  
  static getAllStrategies(): Map<string, DiscountStrategy> {
    return new Map(this.strategies);
  }
  
  static registerStrategy(type: string, strategy: DiscountStrategy): void {
    this.strategies.set(type, strategy);
  }
}

// 简化使用
function calculateDiscountedPrice(price: number, discountType: string): number {
  const strategy = DiscountStrategyFactory.getStrategy(discountType);
  const calculator = new PriceCalculator(strategy);
  return calculator.calculatePrice(price);
}

🚀 React Hooks 中的策略模式

表单验证策略

typescript 复制代码
// 验证策略接口
interface ValidationStrategy {
  validate(value: string): { isValid: boolean; message: string };
}

// 具体验证策略
class EmailValidationStrategy implements ValidationStrategy {
  validate(value: string) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return {
      isValid: emailRegex.test(value),
      message: emailRegex.test(value) ? '' : '请输入有效的邮箱地址'
    };
  }
}

class PhoneValidationStrategy implements ValidationStrategy {
  validate(value: string) {
    const phoneRegex = /^1[3-9]\d{9}$/;
    return {
      isValid: phoneRegex.test(value),
      message: phoneRegex.test(value) ? '' : '请输入有效的手机号码'
    };
  }
}

class PasswordValidationStrategy implements ValidationStrategy {
  validate(value: string) {
    const isValid = value.length >= 8 && /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value);
    return {
      isValid,
      message: isValid ? '' : '密码至少8位,包含大小写字母和数字'
    };
  }
}

class RequiredValidationStrategy implements ValidationStrategy {
  validate(value: string) {
    const isValid = value.trim().length > 0;
    return {
      isValid,
      message: isValid ? '' : '此字段为必填项'
    };
  }
}

// 验证策略工厂
class ValidationStrategyFactory {
  private static strategies = new Map<string, ValidationStrategy>([
    ['email', new EmailValidationStrategy()],
    ['phone', new PhoneValidationStrategy()],
    ['password', new PasswordValidationStrategy()],
    ['required', new RequiredValidationStrategy()]
  ]);
  
  static getStrategy(type: string): ValidationStrategy {
    const strategy = this.strategies.get(type);
    if (!strategy) {
      throw new Error(`Unknown validation type: ${type}`);
    }
    return strategy;
  }
}

// React Hook 实现
import { useState, useCallback } from 'react';

interface UseValidationOptions {
  validationType: string;
  required?: boolean;
}

export const useValidation = ({ validationType, required = false }: UseValidationOptions) => {
  const [error, setError] = useState<string>('');
  const [isValid, setIsValid] = useState<boolean>(true);
  
  const validate = useCallback((value: string) => {
    try {
      // 必填验证
      if (required) {
        const requiredStrategy = ValidationStrategyFactory.getStrategy('required');
        const requiredResult = requiredStrategy.validate(value);
        if (!requiredResult.isValid) {
          setError(requiredResult.message);
          setIsValid(false);
          return false;
        }
      }
      
      // 如果值为空且不是必填,则认为有效
      if (!value.trim() && !required) {
        setError('');
        setIsValid(true);
        return true;
      }
      
      // 具体类型验证
      const strategy = ValidationStrategyFactory.getStrategy(validationType);
      const result = strategy.validate(value);
      
      setError(result.message);
      setIsValid(result.isValid);
      return result.isValid;
    } catch (error) {
      setError('验证配置错误');
      setIsValid(false);
      return false;
    }
  }, [validationType, required]);
  
  const clearError = useCallback(() => {
    setError('');
    setIsValid(true);
  }, []);
  
  return {
    error,
    isValid,
    validate,
    clearError
  };
};

// 使用示例
const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const emailValidation = useValidation({ 
    validationType: 'email', 
    required: true 
  });
  const passwordValidation = useValidation({ 
    validationType: 'password', 
    required: true 
  });
  
  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    emailValidation.validate(value);
  };
  
  const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setPassword(value);
    passwordValidation.validate(value);
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    const isEmailValid = emailValidation.validate(email);
    const isPasswordValid = passwordValidation.validate(password);
    
    if (isEmailValid && isPasswordValid) {
      // 提交表单
      console.log('表单提交成功');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={email}
          onChange={handleEmailChange}
          placeholder="邮箱"
        />
        {emailValidation.error && (
          <span className="error">{emailValidation.error}</span>
        )}
      </div>
      
      <div>
        <input
          type="password"
          value={password}
          onChange={handlePasswordChange}
          placeholder="密码"
        />
        {passwordValidation.error && (
          <span className="error">{passwordValidation.error}</span>
        )}
      </div>
      
      <button 
        type="submit"
        disabled={!emailValidation.isValid || !passwordValidation.isValid}
      >
        登录
      </button>
    </form>
  );
};

🎨 主题切换策略模式

typescript 复制代码
// 主题策略接口
interface ThemeStrategy {
  name: string;
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
    border: string;
  };
  fonts: {
    primary: string;
    secondary: string;
  };
  spacing: {
    small: string;
    medium: string;
    large: string;
  };
  applyTheme(): void;
  getCSS(): string;
}

// 具体主题策略
class LightThemeStrategy implements ThemeStrategy {
  name = 'light';
  colors = {
    primary: '#007AFF',
    secondary: '#5856D6',
    background: '#FFFFFF',
    text: '#000000',
    border: '#E5E5E7'
  };
  fonts = {
    primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
    secondary: 'Georgia, serif'
  };
  spacing = {
    small: '8px',
    medium: '16px',
    large: '24px'
  };
  
  applyTheme(): void {
    const root = document.documentElement;
    Object.entries(this.colors).forEach(([key, value]) => {
      root.style.setProperty(`--color-${key}`, value);
    });
    Object.entries(this.fonts).forEach(([key, value]) => {
      root.style.setProperty(`--font-${key}`, value);
    });
    Object.entries(this.spacing).forEach(([key, value]) => {
      root.style.setProperty(`--spacing-${key}`, value);
    });
  }
  
  getCSS(): string {
    return `
      :root {
        --color-primary: ${this.colors.primary};
        --color-secondary: ${this.colors.secondary};
        --color-background: ${this.colors.background};
        --color-text: ${this.colors.text};
        --color-border: ${this.colors.border};
        --font-primary: ${this.fonts.primary};
        --font-secondary: ${this.fonts.secondary};
        --spacing-small: ${this.spacing.small};
        --spacing-medium: ${this.spacing.medium};
        --spacing-large: ${this.spacing.large};
      }
    `;
  }
}

class DarkThemeStrategy implements ThemeStrategy {
  name = 'dark';
  colors = {
    primary: '#0A84FF',
    secondary: '#5E5CE6',
    background: '#000000',
    text: '#FFFFFF',
    border: '#38383A'
  };
  fonts = {
    primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
    secondary: 'Georgia, serif'
  };
  spacing = {
    small: '8px',
    medium: '16px',
    large: '24px'
  };
  
  applyTheme(): void {
    const root = document.documentElement;
    Object.entries(this.colors).forEach(([key, value]) => {
      root.style.setProperty(`--color-${key}`, value);
    });
    Object.entries(this.fonts).forEach(([key, value]) => {
      root.style.setProperty(`--font-${key}`, value);
    });
    Object.entries(this.spacing).forEach(([key, value]) => {
      root.style.setProperty(`--spacing-${key}`, value);
    });
  }
  
  getCSS(): string {
    return `
      :root {
        --color-primary: ${this.colors.primary};
        --color-secondary: ${this.colors.secondary};
        --color-background: ${this.colors.background};
        --color-text: ${this.colors.text};
        --color-border: ${this.colors.border};
        --font-primary: ${this.fonts.primary};
        --font-secondary: ${this.fonts.secondary};
        --spacing-small: ${this.spacing.small};
        --spacing-medium: ${this.spacing.medium};
        --spacing-large: ${this.spacing.large};
      }
    `;
  }
}

class HighContrastThemeStrategy implements ThemeStrategy {
  name = 'high-contrast';
  colors = {
    primary: '#FFFF00',
    secondary: '#00FFFF',
    background: '#000000',
    text: '#FFFFFF',
    border: '#FFFFFF'
  };
  fonts = {
    primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
    secondary: 'Georgia, serif'
  };
  spacing = {
    small: '12px',
    medium: '20px',
    large: '32px'
  };
  
  applyTheme(): void {
    const root = document.documentElement;
    Object.entries(this.colors).forEach(([key, value]) => {
      root.style.setProperty(`--color-${key}`, value);
    });
    Object.entries(this.fonts).forEach(([key, value]) => {
      root.style.setProperty(`--font-${key}`, value);
    });
    Object.entries(this.spacing).forEach(([key, value]) => {
      root.style.setProperty(`--spacing-${key}`, value);
    });
  }
  
  getCSS(): string {
    return `
      :root {
        --color-primary: ${this.colors.primary};
        --color-secondary: ${this.colors.secondary};
        --color-background: ${this.colors.background};
        --color-text: ${this.colors.text};
        --color-border: ${this.colors.border};
        --font-primary: ${this.fonts.primary};
        --font-secondary: ${this.fonts.secondary};
        --spacing-small: ${this.spacing.small};
        --spacing-medium: ${this.spacing.medium};
        --spacing-large: ${this.spacing.large};
      }
    `;
  }
}

// 主题管理器
class ThemeManager {
  private strategy: ThemeStrategy;
  private static strategies = new Map<string, ThemeStrategy>([
    ['light', new LightThemeStrategy()],
    ['dark', new DarkThemeStrategy()],
    ['high-contrast', new HighContrastThemeStrategy()]
  ]);
  
  constructor(defaultTheme: string = 'light') {
    this.strategy = ThemeManager.strategies.get(defaultTheme) || new LightThemeStrategy();
  }
  
  setTheme(themeName: string): void {
    const strategy = ThemeManager.strategies.get(themeName);
    if (strategy) {
      this.strategy = strategy;
      this.strategy.applyTheme();
    }
  }
  
  getCurrentTheme(): ThemeStrategy {
    return this.strategy;
  }
  
  static registerTheme(name: string, strategy: ThemeStrategy): void {
    this.strategies.set(name, strategy);
  }
  
  static getAvailableThemes(): string[] {
    return Array.from(this.strategies.keys());
  }
}

// React Hook 实现
import { useState, useEffect, createContext, useContext } from 'react';

const ThemeContext = createContext<{
  currentTheme: ThemeStrategy;
  setTheme: (themeName: string) => void;
  availableThemes: string[];
} | null>(null);

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [themeManager] = useState(() => new ThemeManager());
  const [currentTheme, setCurrentTheme] = useState<ThemeStrategy>(
    themeManager.getCurrentTheme()
  );
  
  const setTheme = (themeName: string) => {
    themeManager.setTheme(themeName);
    setCurrentTheme(themeManager.getCurrentTheme());
  };
  
  const availableThemes = ThemeManager.getAvailableThemes();
  
  // 初始化主题
  useEffect(() => {
    themeManager.getCurrentTheme().applyTheme();
  }, [themeManager]);
  
  return (
    <ThemeContext.Provider value={{ currentTheme, setTheme, availableThemes }}>
      {children}
    </ThemeContext.Provider>
  );
};

// 使用示例
const ThemeSelector = () => {
  const { currentTheme, setTheme, availableThemes } = useTheme();
  
  return (
    <select 
      value={currentTheme.name}
      onChange={(e) => setTheme(e.target.value)}
    >
      {availableThemes.map(theme => (
        <option key={theme} value={theme}>
          {theme.charAt(0).toUpperCase() + theme.slice(1)}
        </option>
      ))}
    </select>
  );
};

📊 数据处理策略模式

typescript 复制代码
// 数据处理策略接口
interface DataProcessingStrategy<T, R> {
  process(data: T[]): R;
  getName(): string;
}

// 统计策略
class SumStrategy implements DataProcessingStrategy<number, number> {
  process(data: number[]): number {
    return data.reduce((sum, num) => sum + num, 0);
  }
  
  getName(): string {
    return 'sum';
  }
}

class AverageStrategy implements DataProcessingStrategy<number, number> {
  process(data: number[]): number {
    if (data.length === 0) return 0;
    return data.reduce((sum, num) => sum + num, 0) / data.length;
  }
  
  getName(): string {
    return 'average';
  }
}

class MaxStrategy implements DataProcessingStrategy<number, number> {
  process(data: number[]): number {
    return Math.max(...data);
  }
  
  getName(): string {
    return 'max';
  }
}

class MinStrategy implements DataProcessingStrategy<number, number> {
  process(data: number[]): number {
    return Math.min(...data);
  }
  
  getName(): string {
    return 'min';
  }
}

// 分组策略
class GroupByStrategy<T> implements DataProcessingStrategy<T, Map<string, T[]>> {
  constructor(private keyExtractor: (item: T) => string) {}
  
  process(data: T[]): Map<string, T[]> {
    return data.reduce((groups, item) => {
      const key = this.keyExtractor(item);
      if (!groups.has(key)) {
        groups.set(key, []);
      }
      groups.get(key)!.push(item);
      return groups;
    }, new Map<string, T[]>());
  }
  
  getName(): string {
    return 'groupBy';
  }
}

// 排序策略
class SortStrategy<T> implements DataProcessingStrategy<T, T[]> {
  constructor(
    private compareFn: (a: T, b: T) => number,
    private direction: 'asc' | 'desc' = 'asc'
  ) {}
  
  process(data: T[]): T[] {
    const sorted = [...data].sort(this.compareFn);
    return this.direction === 'desc' ? sorted.reverse() : sorted;
  }
  
  getName(): string {
    return `sort-${this.direction}`;
  }
}

// 数据处理器
class DataProcessor<T> {
  private strategy: DataProcessingStrategy<T, any>;
  
  constructor(strategy: DataProcessingStrategy<T, any>) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: DataProcessingStrategy<T, any>): void {
    this.strategy = strategy;
  }
  
  process(data: T[]) {
    return this.strategy.process(data);
  }
  
  getStrategyName(): string {
    return this.strategy.getName();
  }
}

// React Hook 实现
interface UseDataProcessorOptions<T> {
  data: T[];
  initialStrategy: DataProcessingStrategy<T, any>;
}

export const useDataProcessor = <T>({ data, initialStrategy }: UseDataProcessorOptions<T>) => {
  const [processor] = useState(() => new DataProcessor(initialStrategy));
  const [result, setResult] = useState<any>(null);
  const [currentStrategy, setCurrentStrategy] = useState(initialStrategy.getName());
  
  const processData = useCallback((strategy?: DataProcessingStrategy<T, any>) => {
    if (strategy) {
      processor.setStrategy(strategy);
      setCurrentStrategy(strategy.getName());
    }
    const processedResult = processor.process(data);
    setResult(processedResult);
    return processedResult;
  }, [data, processor]);
  
  useEffect(() => {
    processData();
  }, [processData]);
  
  return {
    result,
    currentStrategy,
    processData
  };
};

// 使用示例
const DataDashboard = () => {
  const salesData = [100, 200, 150, 300, 250, 400];
  
  const {
    result: sumResult,
    processData: processSum
  } = useDataProcessor({
    data: salesData,
    initialStrategy: new SumStrategy()
  });
  
  const {
    result: avgResult,
    processData: processAvg
  } = useDataProcessor({
    data: salesData,
    initialStrategy: new AverageStrategy()
  });
  
  const {
    result: maxResult,
    processData: processMax
  } = useDataProcessor({
    data: salesData,
    initialStrategy: new MaxStrategy()
  });
  
  return (
    <div className="dashboard">
      <h2>销售数据分析</h2>
      <div className="stats">
        <div className="stat-card">
          <h3>总销售额</h3>
          <p>${sumResult}</p>
        </div>
        <div className="stat-card">
          <h3>平均销售额</h3>
          <p>${avgResult?.toFixed(2)}</p>
        </div>
        <div className="stat-card">
          <h3>最高销售额</h3>
          <p>${maxResult}</p>
        </div>
      </div>
      
      <div className="controls">
        <button onClick={() => processSum(new SumStrategy())}>
          重新计算总和
        </button>
        <button onClick={() => processAvg(new AverageStrategy())}>
          重新计算平均值
        </button>
        <button onClick={() => processMax(new MaxStrategy())}>
          重新计算最大值
        </button>
      </div>
    </div>
  );
};

🔄 请求重试策略模式

typescript 复制代码
// 重试策略接口
interface RetryStrategy {
  shouldRetry(attempt: number, error: Error): boolean;
  getDelay(attempt: number): number;
  getName(): string;
}

// 具体重试策略
class LinearRetryStrategy implements RetryStrategy {
  constructor(
    private maxAttempts: number = 3,
    private baseDelay: number = 1000
  ) {}
  
  shouldRetry(attempt: number, error: Error): boolean {
    return attempt < this.maxAttempts;
  }
  
  getDelay(attempt: number): number {
    return this.baseDelay * attempt;
  }
  
  getName(): string {
    return 'linear';
  }
}

class ExponentialRetryStrategy implements RetryStrategy {
  constructor(
    private maxAttempts: number = 3,
    private baseDelay: number = 1000,
    private multiplier: number = 2
  ) {}
  
  shouldRetry(attempt: number, error: Error): boolean {
    return attempt < this.maxAttempts;
  }
  
  getDelay(attempt: number): number {
    return this.baseDelay * Math.pow(this.multiplier, attempt - 1);
  }
  
  getName(): string {
    return 'exponential';
  }
}

class FixedRetryStrategy implements RetryStrategy {
  constructor(
    private maxAttempts: number = 3,
    private delay: number = 1000
  ) {}
  
  shouldRetry(attempt: number, error: Error): boolean {
    return attempt < this.maxAttempts;
  }
  
  getDelay(attempt: number): number {
    return this.delay;
  }
  
  getName(): string {
    return 'fixed';
  }
}

class SmartRetryStrategy implements RetryStrategy {
  constructor(private maxAttempts: number = 3) {}
  
  shouldRetry(attempt: number, error: Error): boolean {
    if (attempt >= this.maxAttempts) return false;
    
    // 根据错误类型决定是否重试
    if (error.message.includes('401') || error.message.includes('403')) {
      return false; // 认证错误不重试
    }
    
    if (error.message.includes('429')) {
      return true; // 限流错误需要重试
    }
    
    if (error.message.includes('5')) {
      return true; // 服务器错误重试
    }
    
    return false;
  }
  
  getDelay(attempt: number): number {
    // 智能延迟:基于尝试次数和随机抖动
    const baseDelay = 1000;
    const jitter = Math.random() * 500;
    return baseDelay * Math.pow(2, attempt - 1) + jitter;
  }
  
  getName(): string {
    return 'smart';
  }
}

// 重试执行器
class RetryExecutor {
  constructor(private strategy: RetryStrategy) {}
  
  setStrategy(strategy: RetryStrategy): void {
    this.strategy = strategy;
  }
  
  async execute<T>(operation: () => Promise<T>): Promise<T> {
    let attempt = 1;
    let lastError: Error;
    
    while (true) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (!this.strategy.shouldRetry(attempt, lastError)) {
          throw lastError;
        }
        
        const delay = this.strategy.getDelay(attempt);
        console.log(
          `Attempt ${attempt} failed, retrying in ${delay}ms using ${this.strategy.getName()} strategy`
        );
        
        await new Promise(resolve => setTimeout(resolve, delay));
        attempt++;
      }
    }
  }
}

// React Hook 实现
interface UseRetryOptions {
  strategy?: RetryStrategy;
  onRetry?: (attempt: number, error: Error) => void;
  onSuccess?: (result: any, attempts: number) => void;
  onFailure?: (error: Error, attempts: number) => void;
}

export const useRetry = (options: UseRetryOptions = {}) => {
  const {
    strategy = new ExponentialRetryStrategy(),
    onRetry,
    onSuccess,
    onFailure
  } = options;
  
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [attempts, setAttempts] = useState(0);
  
  const executor = useRef(new RetryExecutor(strategy));
  
  const execute = useCallback(async <T>(operation: () => Promise<T>): Promise<T | null> => {
    setIsLoading(true);
    setError(null);
    setAttempts(0);
    
    try {
      // 包装操作以追踪重试次数
      let currentAttempt = 0;
      const wrappedOperation = async (): Promise<T> => {
        currentAttempt++;
        setAttempts(currentAttempt);
        
        try {
          return await operation();
        } catch (error) {
          onRetry?.(currentAttempt, error as Error);
          throw error;
        }
      };
      
      const result = await executor.current.execute(wrappedOperation);
      onSuccess?.(result, currentAttempt);
      return result;
    } catch (error) {
      const err = error as Error;
      setError(err);
      onFailure?.(err, attempts);
      return null;
    } finally {
      setIsLoading(false);
    }
  }, [onRetry, onSuccess, onFailure, attempts]);
  
  const setStrategy = useCallback((newStrategy: RetryStrategy) => {
    executor.current.setStrategy(newStrategy);
  }, []);
  
  return {
    execute,
    setStrategy,
    isLoading,
    error,
    attempts
  };
};

// 使用示例
const ApiComponent = () => {
  const [data, setData] = useState(null);
  const [retryStrategy, setRetryStrategyType] = useState('exponential');
  
  const retry = useRetry({
    strategy: new ExponentialRetryStrategy(3, 1000),
    onRetry: (attempt, error) => {
      console.log(`Retry attempt ${attempt}:`, error.message);
    },
    onSuccess: (result, attempts) => {
      console.log(`Success after ${attempts} attempts`);
      setData(result);
    },
    onFailure: (error, attempts) => {
      console.error(`Failed after ${attempts} attempts:`, error.message);
    }
  });
  
  const fetchData = async () => {
    await retry.execute(async () => {
      const response = await fetch('/api/data');
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      return response.json();
    });
  };
  
  const changeStrategy = (strategyType: string) => {
    setRetryStrategyType(strategyType);
    
    switch (strategyType) {
      case 'linear':
        retry.setStrategy(new LinearRetryStrategy(3, 1000));
        break;
      case 'exponential':
        retry.setStrategy(new ExponentialRetryStrategy(3, 1000));
        break;
      case 'fixed':
        retry.setStrategy(new FixedRetryStrategy(3, 2000));
        break;
      case 'smart':
        retry.setStrategy(new SmartRetryStrategy(5));
        break;
    }
  };
  
  return (
    <div>
      <h3>API 请求重试示例</h3>
      
      <div>
        <label>重试策略: </label>
        <select 
          value={retryStrategy} 
          onChange={(e) => changeStrategy(e.target.value)}
        >
          <option value="linear">线性延迟</option>
          <option value="exponential">指数退避</option>
          <option value="fixed">固定延迟</option>
          <option value="smart">智能重试</option>
        </select>
      </div>
      
      <button onClick={fetchData} disabled={retry.isLoading}>
        {retry.isLoading ? `请求中... (尝试 ${retry.attempts})` : '获取数据'}
      </button>
      
      {retry.error && (
        <div style={{ color: 'red' }}>
          错误: {retry.error.message}
        </div>
      )}
      
      {data && (
        <div style={{ color: 'green' }}>
          数据获取成功: {JSON.stringify(data)}
        </div>
      )}
    </div>
  );
};

🎯 策略模式的优势

1. 开闭原则

  • 对扩展开放:可以轻松添加新策略
  • 对修改封闭:无需修改现有代码

2. 单一职责原则

  • 每个策略类只负责一种算法
  • 上下文类只负责委托工作

3. 可测试性

typescript 复制代码
// 策略可以独立测试
describe('DiscountStrategy', () => {
  it('should calculate student discount correctly', () => {
    const strategy = new StudentDiscountStrategy();
    expect(strategy.calculate(100)).toBe(80);
  });
  
  it('should calculate VIP discount correctly', () => {
    const strategy = new VipDiscountStrategy();
    expect(strategy.calculate(100)).toBe(70);
  });
});

4. 运行时切换

typescript 复制代码
// 可以在运行时动态切换策略
const calculator = new PriceCalculator(new NoDiscountStrategy());

// 用户登录后切换为相应的折扣策略
if (user.type === 'student') {
  calculator.setStrategy(new StudentDiscountStrategy());
} else if (user.type === 'vip') {
  calculator.setStrategy(new VipDiscountStrategy());
}

⚠️ 注意事项和最佳实践

1. 避免过度设计

typescript 复制代码
// ❌ 简单场景不需要策略模式
function isEven(num: number): boolean {
  return num % 2 === 0;
}

// ✅ 复杂策略才使用策略模式
interface ValidationStrategy {
  validate(data: FormData): ValidationResult;
}

2. 合理使用工厂模式

typescript 复制代码
// 结合工厂模式管理策略
class StrategyFactory {
  private static cache = new Map<string, Strategy>();
  
  static getStrategy(type: string): Strategy {
    if (!this.cache.has(type)) {
      this.cache.set(type, this.createStrategy(type));
    }
    return this.cache.get(type)!;
  }
  
  private static createStrategy(type: string): Strategy {
    // 创建策略的逻辑
  }
}

3. 考虑性能影响

typescript 复制代码
// 使用单例模式避免重复创建策略对象
class SingletonStrategy implements Strategy {
  private static instance: SingletonStrategy;
  
  static getInstance(): SingletonStrategy {
    if (!this.instance) {
      this.instance = new SingletonStrategy();
    }
    return this.instance;
  }
  
  private constructor() {}
}

📋 总结

策略模式是一个强大而优雅的设计模式,特别适合以下场景:

  1. 算法选择:需要在多种算法中选择
  2. 行为变化:对象的行为需要动态改变
  3. 条件语句替换:减少复杂的if-else或switch语句
  4. 扩展性要求:需要频繁添加新的处理方式

通过策略模式,我们可以:

  • 提高代码的可维护性和扩展性
  • 提高代码的可测试性
  • 实现更清晰的代码结构

在实际项目中,策略模式常常与工厂模式、单例模式等其他设计模式结合使用,形成更加强大和灵活的解决方案。

提醒一手:好的设计模式不是为了炫技,而是为了解决实际问题,让代码更加清晰、可维护和可扩展。