场景
进入一个营销活动页面,会根据后端下发的不同 type
,前端页面展示不同的弹窗。
1async getMainData() {
2 try {
3 const res = await activityQuery(); // 请求后端数据
4 this.styleType = res.styleType;
5 if (this.styleType === STYLE_TYPE.Reward) {
6 this.openMoneyPop();
7 }else if (this.styleType === STYLE_TYPE.Waitreward) {
8 this.openShareMoneyPop();
9 } else if (this.styleType === STYLE_TYPE.Poster) {
10 this.openPosterPop();
11 } else if (this.styleType === STYLE_TYPE.Activity) {
12 this.openActivityPop();
13 } else if (this.styleType === STYLE_TYPE.Balance) {
14 this.openBalancePop();
15 } else if (this?.styleType === STYLE_TYPE.Cash) {
16 this.openCashBalancePop();
17 }
18 } catch (error) {
19 log.error(MODULENAME, '主接口异常', JSON.stringify(error));
20 }
21}
这个代码的话看了就想打人,未来新增一种弹窗类型的话,我们需要到 getMainData
内部去补一个 else if
,一不小心可能就会影响到原有的逻辑,并且随着迭代函数会越来越大。但其实每种弹窗是相互独立的,我们并不关心其他弹窗的逻辑。
此时,就需要策略模式了。
策略模式
看下 维基百科 的定义。
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在中国交个人所得税”和“在美国交个人所得税”就有不同的算税方法。
策略模式:
- 定义了一族算法(业务规则);
- 封装了每个算法;
- 这族的算法可互换代替(interchangeable)。
看一下如果是 Java
语言会怎么实现:
1//StrategyExample test application
2class StrategyExample {
3 public static void main(String[] args) {
4 Context context;
5 // Three contexts following different strategies
6 context = new Context(new FirstStrategy());
7 context.execute();
8
9 context = new Context(new SecondStrategy());
10 context.execute();
11
12 context = new Context(new ThirdStrategy());
13 context.execute();
14 }
15}
16// The classes that implement a concrete strategy should implement this
17// The context class uses this to call the concrete strategy
18interface Strategy {
19 void execute();
20}
21
22// Implements the algorithm using the strategy interface
23class FirstStrategy implements Strategy {
24 public void execute() {
25 System.out.println("Called FirstStrategy.execute()");
26 }
27}
28
29class SecondStrategy implements Strategy {
30 public void execute() {
31 System.out.println("Called SecondStrategy.execute()");
32 }
33}
34
35class ThirdStrategy implements Strategy {
36 public void execute() {
37 System.out.println("Called ThirdStrategy.execute()");
38 }
39}
40
41// Configured with a ConcreteStrategy object and maintains a reference to a Strategy object
42class Context {
43 Strategy strategy;
44 // Constructor
45 public Context(Strategy strategy) {
46 this.strategy = strategy;
47 }
48 public void execute() {
49 this.strategy.execute();
50 }
51
52}
主要是利用到类的多态,根据传入 Context
中不同的 strategy
,来执行不同的 execute()
。如果未来有新增算法的话,只需要新增一个类即可。
那如果是 js
呢?众所周知,ES6
之前 js
是没有 class
关键字的,即使现在有了,也依然只是基于原型的语法糖,底层和 java
的类是完全不同的。
此外,js
中函数是一等公民,可以当作参数传入和返回,因此实现策略模式我们完全不需要去定一个类,然后通过生成的对象调用方法。在 js
中我们只需要将函数传入即可。
1const strategies = {
2 FirstStrategy() {
3 console.log('Called FirstStrategy')
4 },
5 SecondStrategy() {
6 console.log('Called SecondStrategy')
7 },
8 ThirdStrategy() {
9 console.log('Called ThirdStrategy')
10 },
11}
12
13function execute(strategy) {
14 return strategies[strategy]()
15}
16
17execute('FirstStrategy')
18execute('SecondStrategy')
19execute('ThirdStrategy')
上边主要演示了思想,实际开发中,我们完全可以把每种策略分文件单独写然后再 import
。
相对于 java
,写法简单了很多,我们不需要定义各个类,只需要用一个对象来存储所有策略,再提供一个调用策略的函数,甚至这个函数也可以直接省略。
优化代码
将所有弹窗方法从业务代码 getMainData
中抽离出来,只暴露一个打开弹窗的函数供业务调用。
1import { openPop } from './popTypes';
2async getMainData() {
3 try {
4 const res = await activityQuery(); // 请求后端数据
5 openPop(res.styleType)
6 } catch (error) {
7 log.error(MODULENAME, '主接口异常', JSON.stringify(error));
8 }
9}
然后就是 popTypes.js
文件。
1import { SHARETYPE } from './constant';
2
3const popTypes = {
4 [STYLE_TYPE.Reward]: function() {
5 ...
6 },
7 [STYLE_TYPE.Waitreward]: function() {
8 ...
9 },
10 [STYLE_TYPE.Poster]: function() {
11 ...
12 },
13 [STYLE_TYPE.Activity]: function() {
14 ...
15 },
16 [STYLE_TYPE.Balance]: function() {
17 ...
18 },
19 [STYLE_TYPE.Cash]: function() {
20 ...
21 },
22}
23
24export function openPop(type){
25 return popTypes[type]();
26}
更多场景
表单验证也是一个典型场景,常用的,我们需要验证用户输入字段是否是数字、是否必填、是否是数组,还有自定义的一些验证,同样可以通过策略模式实现,从而使得代码更易维护和扩展。
如果使用过 Element UI
,对下边表单的 rule
一定很熟悉。
1const rule = {
2 name: {
3 type: 'string',
4 required: true,
5 message: '请输入名字',
6 },
7 age: [
8 {
9 type: 'number',
10 message: '请输入number',
11 },
12 {
13 message: '年龄必须大于 18',
14 validator: (rule, value) => value > 18,
15 },
16 ],
17}
Element
会帮助我们校验 name
是否是 string
、age
是否是 number
。而 Element
其实是用的一个开源的 async-validator 校验库。
async-validator 内部会内置很多 type
的 validator
,然后会根据 rule
中的 type
来帮我们填充相应的 validator
。让我们看一下相应的源码。
首先是 validator
文件夹,会定义很多校验规则,date
类型、number
类型等等,相当于很多策略。
然后是上边截图中的 validator/index.ts
文件,将这些策略导出。
1import string from './string'
2import method from './method'
3import number from './number'
4import boolean from './boolean'
5import regexp from './regexp'
6import integer from './integer'
7import float from './float'
8import array from './array'
9import object from './object'
10import enumValidator from './enum'
11import pattern from './pattern'
12import date from './date'
13import required from './required'
14import type from './type'
15import any from './any'
16
17export default {
18 string,
19 method,
20 number,
21 boolean,
22 regexp,
23 integer,
24 float,
25 array,
26 object,
27 enum: enumValidator,
28 pattern,
29 date,
30 url: type,
31 hex: type,
32 email: type,
33 required,
34 any,
35}
校验前会执行下边的代码,通过 type
填充相应的 validator
。
1arr.forEach(r => {
2 ...
3
4 if (typeof rule === 'function') {
5 rule = {
6 validator: rule,
7 };
8 } else {
9 rule = { ...rule };
10 }
11
12 // Fill validator. Skip if nothing need to validate
13 rule.validator = this.getValidationMethod(rule); // 策略模式应用
14 if (!rule.validator) {
15 return;
16 }
17
18 ...
19});
20});
策略模式的体现就是 getValidationMethod
方法了,让我们看一下实现。
1import validators from './validator/index'; // 导入所有策略
2
3getValidationMethod(rule: InternalRuleItem) {
4 // 已经有了就直接返回 validator
5 if (typeof rule.validator === 'function') {
6 return rule.validator;
7 }
8 ...
9 // 通过 type 得到相应的 validator。
10 return validators[this.getType(rule)] || undefined;
11}
12
13getType(rule: InternalRuleItem) {
14 ...
15 return rule.type || 'string';
16}
填充相应的 validator
之后接下来只需要遍历相应的 rule
然后校验就可以了。
总
当出现很多 if else
或者 switch
的时候,我们就可以考虑是否能使用策略模式了。
通过策略模式,我们可以把策略从业务代码中抽离出来,未来扩展的话无需深入到业务代码修改,只需要新增需要的策略,不会使得业务代码变得越来越臃肿。
甚至策略模式也可以更好的进行复用,如果其他业务场景需要类似的策略,直接引入即可,和原有的业务相互独立。