拒绝复杂 if-else,前端策略模式实践
设计模式的重要性
为什么要学习和使用设计模式,我觉得原因主要有两点
- 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现
- 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码的说明书
在前端开发过程中面对复杂场景能能够更清晰的处理代码逻辑,其中策略模式在我的前端工作中的应用非常多,下面就展开讲讲策略模式在前端开发的具体应用
策略模式基础
策略模式的含义是:定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换
我个人对于策略模式的理解,就是将原来写在一个函数中一整套功能,拆分为一个个独立的部分,从而达到解耦的目的。所以策略模式最好的应用场景,就是拆解 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
前端策略模式应用
实际上在前端开发中,通常不会使用到面向对象的模式,在前端中应用策略模式,完全可以简化为两个部分
- 对象:存储策略算法,并通过 key 匹配对应算法
- 策略方法:实现 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 匹配具体策略实现,就可以实现一个基础的策略模式