拒绝复杂 if-else,前端策略模式实践

设计模式的重要性

为什么要学习和使用设计模式,我觉得原因主要有两点

  1. 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现
  2. 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码的说明书

在前端开发过程中面对复杂场景能能够更清晰的处理代码逻辑,其中策略模式在我的前端工作中的应用非常多,下面就展开讲讲策略模式在前端开发的具体应用

策略模式基础

策略模式的含义是:定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换

我个人对于策略模式的理解,就是将原来写在一个函数中一整套功能,拆分为一个个独立的部分,从而达到解耦的目的。所以策略模式最好的应用场景,就是拆解 if-else,把每个 if 模块封装为独立算法

在面向对象的语言中,策略模式通常有三个部分

  • 策略(Strategy):实现不同算法的接口
  • 具体策略(Concrete Strategy):实现了策略定义的接口,提供具体的算法实现
  • 上下文(Context):持有一个策略对象的引用,用一个 ConcreteStrategy 对象来配置,维护一个对 Strategy 对象的引用

这么看定义可能不太直观,这里我用 TS 面向对象的方式实现的一个计算器的策略模式例子说明一下

1// 第一步: 定义策略(Strategy) 2interface CalculatorStrategy { 3 calculate(a: number, b: number): number 4} 5 6// 第二步:定义具体策略(Concrete Strategy) 7class AddStrategy implements CalculatorStrategy { 8 calculate(a: number, b: number): number { 9 return a + b 10 } 11} 12 13class SubtractStrategy implements CalculatorStrategy { 14 calculate(a: number, b: number): number { 15 return a - b 16 } 17} 18 19class MultiplyStrategy implements CalculatorStrategy { 20 calculate(a: number, b: number): number { 21 return a * b 22 } 23} 24 25// 第三步: 创建上下文(Context),用于调用不同的策略 26class CalculatorContext { 27 private strategy: CalculatorStrategy 28 29 constructor(strategy: CalculatorStrategy) { 30 this.strategy = strategy 31 } 32 33 setStrategy(strategy: CalculatorStrategy) { 34 this.strategy = strategy 35 } 36 37 calculate(a: number, b: number): number { 38 return this.strategy.calculate(a, b) 39 } 40} 41 42// 使用策略模式进行计算 43const addStrategy = new AddStrategy() 44const subtractStrategy = new SubtractStrategy() 45const multiplyStrategy = new MultiplyStrategy() 46 47const calculator = new CalculatorContext(addStrategy) 48console.log(calculator.calculate(5, 3)) // 输出 8 49 50calculator.setStrategy(subtractStrategy) 51console.log(calculator.calculate(5, 3)) // 输出 2 52 53calculator.setStrategy(multiplyStrategy) 54console.log(calculator.calculate(5, 3)) // 输出 15

前端策略模式应用

实际上在前端开发中,通常不会使用到面向对象的模式,在前端中应用策略模式,完全可以简化为两个部分

  1. 对象:存储策略算法,并通过 key 匹配对应算法
  2. 策略方法:实现 key 对应的具体策略算法

这里举一个在最近开发过程应用策略模式重构的例子,实现的功能是对于不同的操作,处理相关字段的联动,在原始代码中,对于操作类型 opType 使用大量 if-else 判断,代码大概是这样的,虽然看起来比较少,但是每个 if 里面都有很多处理逻辑的话,整体的可读性的就会非常差了

1export function transferAction() { 2 actions.forEach((action) => { 3 const { opType } = action 4 5 // 展示 / 隐藏字段 6 if (opType === OP_TYPE_KV.SHOW) { } 7 else if (opType === OP_TYPE_KV.HIDE) {} 8 // 启用 / 禁用字段 9 else if (opType === OP_TYPE_KV.ENABLE) { } 10 else if (opType === OP_TYPE_KV.DISABLE) {} 11 // 必填 / 非必填字段 12 else if (opType === OP_TYPE_KV.REQUIRED) { } 13 else if ((opType === OP_TYPE_KV.UN_REQUIRED) { } 14 // 清空字段值 15 else if (opType === OP_TYPE_KV.CLEAR && isSatify) { } 16 }) 17}

在使用策略模式重构之后,将每个 action 封装进单独的方法,再把所用的算法放入一个对象,通过触发条件匹配。这样经过重构后的代码,相比于原来的 if-else 结构更清晰,每次只要找到对应的策略方法实现即可。并且如果后续有扩展,只要继续新的增加策略方法就好,不会影响到老的代码

1export function transferAction(/* 参数 */) { 2 /** 3 * @description 处理字段显示和隐藏 4 */ 5 const handleShowAndHide = ({ opType, relativeGroupCode, relativeCode }) => {} 6 7 /** 8 * @description // 启用、禁用字段(支持表格行字段的联动) 9 */ 10 const handleEnableAndDisable = ({ 11 opType, 12 relativeGroupCode, 13 relativeCode, 14 }) => {} 15 16 /** 17 * @description 必填 / 非必填字段(支持表格行字段的联动) 18 */ 19 const handleRequiredAndUnrequired = ({ 20 opType, 21 relativeGroupCode, 22 relativeCode, 23 }) => {} 24 25 /** 26 * @description 清空字段值 27 */ 28 const handleClear = ({ opType, relativeGroupCode, relativeCode }) => {} 29 30 // 联动策略 31 const strategyMap = { 32 // 显示、隐藏 33 [OP_TYPE_KV.SHOW]: handleShowAndHide, 34 [OP_TYPE_KV.HIDE]: handleShowAndHide, 35 // 禁用、启用 36 [OP_TYPE_KV.ENABLE]: handleEnableAndDisable, 37 [OP_TYPE_KV.DISABLE]: handleEnableAndDisable, 38 // 必填、非必填 39 [OP_TYPE_KV.REQUIRED]: handleRequiredAndUnrequired, 40 [OP_TYPE_KV.UN_REQUIRED]: handleRequiredAndUnrequired, 41 // 清空字段值 42 [OP_TYPE_KV.CLEAR]: handleClear, 43 } 44 45 // 遍历执行联动策略 46 actions.forEach((action) => { 47 const { opType, relativeGroupCode, relativeCode, value } = action 48 49 if (strategyMap[opType]) { 50 strategyMap[opType]({ 51 /* 入参 */ 52 }) 53 } 54 }) 55}

总结

策略模式的优点在于:代码逻辑更清晰,每个策略对对应一个实现方法;同时遵循开闭原则,新的策略方法无需改变已有代码,所以非常适合处理或重构复杂逻辑的 if-else

在前端开发过程中,不需要遵循面向对象的应用方式,只需要通过对象存储策略算法,通过 key 匹配具体策略实现,就可以实现一个基础的策略模式