场景

进入一个营销活动页面,会根据后端下发的不同 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 是否是 stringage 是否是 number。而 Element 其实是用的一个开源的 async-validator 校验库。

async-validator 内部会内置很多 typevalidator,然后会根据 rule 中的 type 来帮我们填充相应的 validator。让我们看一下相应的源码。

首先是 validator 文件夹,会定义很多校验规则,date 类型、number 类型等等,相当于很多策略。

image-20220106090411041

然后是上边截图中的 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 的时候,我们就可以考虑是否能使用策略模式了。

通过策略模式,我们可以把策略从业务代码中抽离出来,未来扩展的话无需深入到业务代码修改,只需要新增需要的策略,不会使得业务代码变得越来越臃肿。

甚至策略模式也可以更好的进行复用,如果其他业务场景需要类似的策略,直接引入即可,和原有的业务相互独立。