← 返回导航

策略模式

设计模式 · 实战

场景说明

策略模式是消除 if-else/switch 的经典手段:把每种算法/逻辑封装成独立的策略函数,用一个注册表(Record)把策略名映射到策略函数,执行时根据名称查表调用。TypeScript 下的策略模式可以用 Record 做类型化注册表,用泛型约束策略参数和返回值,新增策略只需加一条记录而无需修改执行器,完美符合开闭原则。表单校验、折扣计算、支付方式、文件上传等场景都非常适合用策略模式重构。

完整代码


      
// 策略模式:策略注册表 + 执行器,Record 映射策略名到策略函数
// 泛型约束策略参数和返回值,消除 if-else,符合开闭原则

// 1. 策略函数类型:输入参数 + 返回值
type Strategy<TParams = any, TResult = any> =
  (params: TParams) => TResult;

// 2. 策略注册表:策略名 → 策略函数
type StrategyRegistry<K extends string = string, P = any, R = any> =
  Record<K, Strategy<P, R>>;

// 3. 策略执行器:根据策略名执行对应策略
class StrategyExecutor<K extends string, P, R> {
  private strategies: StrategyRegistry<K, P, R>;

  constructor(strategies: StrategyRegistry<K, P, R>) {
    this.strategies = strategies;
  }

  // 注册新策略
  register(name: K, strategy: Strategy<P, R>): void {
    this.strategies[name] = strategy;
  }

  // 执行策略
  execute(name: K, params: P): R {
    const strategy = this.strategies[name];
    if (!strategy) {
      throw new Error(`Strategy ${name} not found`);
    }
    return strategy(params);
  }

  // 检查策略是否存在
  has(name: string): name is K {
    return name in this.strategies;
  }

  // 获取所有策略名
  getStrategyNames(): K[] {
    return Object.keys(this.strategies) as K[];
  }
}

// 4. 实战示例:折扣计算器

// 折扣类型
type DiscountType = 'none' | 'percentage' | 'fixed' | 'vip';

// 折扣参数
interface DiscountParams {
  originalPrice: number;
  discountValue: number;  // 百分比值 / 固定减免金额 / VIP 折扣率
}

// 折扣策略注册表
const discountStrategies: StrategyRegistry<DiscountType, DiscountParams, number> = {
  none: ({ originalPrice }) => originalPrice,
  percentage: ({ originalPrice, discountValue }) => originalPrice * (discountValue / 100),
  fixed: ({ originalPrice, discountValue }) => Math.max(0, originalPrice - discountValue),
  vip: ({ originalPrice, discountValue }) => originalPrice * (discountValue / 100) * 0.95,  // VIP 折上折
};

// 创建折扣计算器
const discountCalculator = new StrategyExecutor(discountStrategies);

// 5. 实战示例:表单校验策略

type ValidatorType = 'required' | 'email' | 'phone' | 'minLength';

interface ValidatorParams {
  value: string;
  minLength?: number;
}

interface ValidatorResult {
  valid: boolean;
  message: string;
}

const validatorStrategies: StrategyRegistry<ValidatorType, ValidatorParams, ValidatorResult> = {
  required: ({ value }) => ({
    valid: value.trim().length > 0,
    message: '该字段不能为空',
  }),
  email: ({ value }) => ({
    valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: '请输入有效的邮箱地址',
  }),
  phone: ({ value }) => ({
    valid: /^1\d{10}$/.test(value),
    message: '请输入有效的手机号',
  }),
  minLength: ({ value, minLength = 0 }) => ({
    valid: value.length >= minLength,
    message: `长度至少为 ${minLength} 个字符`,
  }),
};

const validatorExecutor = new StrategyExecutor(validatorStrategies);

// 使用
const price = discountCalculator.execute('percentage', {
  originalPrice: 100,
  discountValue: 80,
}); // = 80

const emailResult = validatorExecutor.execute('email', {
  value: 'test@example.com',
}); // = { valid: true, message: '...' }

关键点解析

使用示例与类型推导


      
// 策略名有类型提示和约束
const result = discountCalculator.execute('none', {
  originalPrice: 200,
  discountValue: 0,
});
// result: number

// 传错策略名会报错
// discountCalculator.execute('unknown', { ... });  // 错误

// 运行时检查 + 类型收窄
function safeExecute(name: string, params: DiscountParams) {
  if (discountCalculator.has(name)) {
    // 这里 name 的类型被收窄为 DiscountType
    return discountCalculator.execute(name, params);
  }
  throw new Error('未知的折扣类型');
}

// 获取所有策略名
const allDiscounts = discountCalculator.getStrategyNames();
// => ['none', 'percentage', 'fixed', 'vip']
// 类型:DiscountType[]

// 运行时注册新策略
discountCalculator.register('secondHalf', ({ originalPrice, discountValue }) => {
  const fullPrice = Math.floor(originalPrice / 2);
  const halfPrice = originalPrice - fullPrice;
  return fullPrice + halfPrice * (discountValue / 100);
});
// 动态注册后就可以 execute('secondHalf', ...)

// 对比:传统 if-else 写法
function calcDiscount(type: string, price: number, value: number): number {
  if (type === 'none') return price;
  if (type === 'percentage') return price * (value / 100);
  if (type === 'fixed') return Math.max(0, price - value);
  if (type === 'vip') return price * (value / 100) * 0.95;
  throw new Error('unknown type');
}
// 缺点:每加一个策略就要修改函数,违反开闭原则
// 策略模式:只需 register 注册,执行器代码不动

// 实战:表单校验函数
function validateField(value: string, rules: ValidatorType[]): ValidatorResult {
  for (const rule of rules) {
    const result = validatorExecutor.execute(rule, { value, minLength: 6 });
    if (!result.valid) return result;
  }
  return { valid: true, message: '' };
}

const r = validateField('abc123', ['required', 'minLength']);
// r.valid = true
在 Playground 中尝试