新生代农民工也要懂的策略设计模式

字节前端 ByteFE 2021-10-14 18:20

俗话说,凡事讲策略。讲策略的时候,我们往往会考虑每种情况的成本。策略同样可体现在我们的代码之中,合理利用策略模式重构逻辑复杂的代码,会使项目工程更易维护和扩展。


这几天朋友圈被“新生代农民工”刷屏了,看到有这样一张截图:


代码里写了约 30 个if else逻辑,从程序语义以及程序效率理论上是会有一定的影响,最主要的是可能会被其他“新生代农民工”嘲笑


一位经验老道的民工则会用一手switch case或策略模式来重构代码,那么什么是策略模式呐?

一、定义

策略:为实现一定的战略任务,根据形势发展而制定的行动方针和斗争方式。策略模式:一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。在常见的前端游戏奖励激励交互中,常常会涉及到不同分数会展示不同的动效,这其实就是一种条件策略。

二、优缺点

优点:

  1. 隔离算法的实现与使用
  2. 运行时可切换算法
  3. 用组合代替继承
  4. 易扩展,符合开闭原则,无需修改上下文即可引入新策略

缺点:

  1. 需要暴露所有的策略接口,用于区分策略差异
  2. 如果逻辑条件简单,使用策略模式会增加代码冗余度

三、实现

策略模式指的是定义了一系列算法,把它们每个都封装起来。将不变的部分和变化的部分隔离开来是设计模式中的一个重要思想,策略模式则是将算法和使用算法两部分的实现拆开,降低耦合度。基于策略模式的程序至少有两部分组成:策略类和环境类(Context)。策略类封装了具体的算法,并负责具体的计算过程,可以理解为“执行者”。环境类(Context)接受客户的请求,然后将请求委托给一个策略类,可以理解为“调度者”。

四、表单验证中的策略模式

在 Web 项目中,常见的表单有注册、登陆、修改用户信息等涉及到表单的功能,与此同时我们会在表单提交的时候,做一些例的前端输入框值的条件校验工作。由于输入框中用户的输入是任意的,校验的规则相对比较复杂,如果不使用设计模式,我们的代码中可能就会写出较多的if else判断逻辑,从可阅读性和可维护性来说,确实不是很好。接下来我们将从前端 Web 项目中常见的表单验证功能,逐步认识策略设计模式。

4.1 初级的表单验证

在很久很久以前,我的表单验证可能是这么写的:

var username = $('#nuserame').val();
var password = $('#password').val();

if (!username) {
    alert('用户名不能为空');
else if (username.length < 5) {
    alert('用户名长度需要大于等于5');
else if (username.length < 13) {
    alert('用户名长度需要小于13');
else if (!(/[a-z]+/i.test(username))) {
    alert('用户名只能包含英文大小写字符')
else {
    regeister(username);
}

// password的验证同上

写法似乎有些不忍直视,不过能用!

4.2 基于策略模式的表单验证

换个思路,结合策略模式的思想,实现一个专用于值校验的Validator类,Validator是一个调度者,也就是策略模式中的环境类。然后我们在验证目标字段值targetValue的时候其用法大致如下:

Validator.addRules(targetValue['isNonEmpty''minLength:5''maxLength:12']).valid();

校验器会返回判断结果result字段,以及语义话的提示msg字段:

return {
    result: false,
    msg: '不能为空'
}

4.2.1 Validator

根据上述需求,Validator 的实现如下:

const formatResult = (isPass: boolean = false, errMsg: string = "") => {
  return {
    result: isPass,
    msg: errMsg,
  };
};

const ValidStrategies = {
  isNonEmpty: function (val: string = ""{
    if (!val) return formatResult(false"内容不能为空");
  },
  minLength: function (val: string = "", length: string | number = 0{
    console.log(val, length);
    if (typeof length === "string") length = parseInt(length);
    if (val.length < length)
      return formatResult(false`内容长度不能小于 ${length}`);
  },
  maxLength: function (val: string = "", length: string | number = 0{
    if (typeof length === "string") length = parseInt(length);
    if (val.length > length)
      return formatResult(false`内容长度不能大于 ${length}`);
  },
  defaultfunction ({
    return formatResult(true);
  },
};

/**
 * 验证器
 * 策略模式 —— 环境类,负责调度算法
 */

class Validator {
  // 存储规则
  private _ruleExecuters: Array<any>;

  constructor() {
    this._ruleExecuters = [];
  }

  /**
   * addRules
   * 添加校验规则
   */

  public addRules(value: string = "", rules: Array<string>) {
    this._ruleExecuters = [];
    rules.forEach((rule) => {
      const args = rule.split(":");
      const functionName = args.shift() || "default";
      // 忽略下这里的断言类型👀
      const ruleFunc = ValidStrategies[
        functionName as "isNonEmpty" | "minLength" | "maxLength" | "default"
      ].bind(this, value);
      this._ruleExecuters.push({
        func: ruleFunc,
        args,
      });
    });
    return this;
  }

  /**
   * valid
   */

  public valid() {
    for (let i = 0; i < this._ruleExecuters.length; i++) {
      const res = this._ruleExecuters[i].func.apply(
        this,
        this._ruleExecuters[i].args
      );
      if (res && !res.result) {
        return res;
      }
    }
    return formatResult(true);
  }
}

export default new Validator();
const res = Validator.addRules("123", [
    "isNonEmpty",
    "minLength:5",
    "maxLength:12",
  ]).valid();
  console.log("res:", res);

这样在验证表单值的时候,我们就可以直接调用 Validator 验证值的合法性。与此同时,还可以通过扩展策略类(对象)ValidStrategies中的验证算法来扩展校验器的能力。

五、表驱动法

策略模式节省逻辑判断的特性让我联想到了之前看过的一个概念“表驱动法”,或者叫“查表法”,这里引用下百度百科的解释:

表驱动方法出于特定的目的来使用表,程序员们经常谈到“表驱动”方法,但是课本中却从未提到过什么是"表驱动"方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句ifCase)来把它们找出来的方法。事实上,任何信息都可以通过表来挑选。在简单的情况下,逻辑语句往往更简单而且更直接。但随着逻辑链的复杂,表就变得越来越富有吸引力了。

举个 🌰,假设我们想要获取当前是星期几,代码可能是这样的:

function getDay({
  const day = (new Date()).getDay();
  switch (day) {
    case 0:
      return '星期日';
    case 1:
      return '星期一';
    // ...
    case 6:
      return '星期六';
    default:
      return '';
  }
}
```Javascript

如果是初次编程的同学还可能会有`
if else`条件语句来判断返回值,代码就会显得比较冗余。借助表驱动发法的思想,这里我们是可以有优化空间的,表驱动发法的写法如下:

`
``js
const days = ['星期日''星期一''星期二''星期三''星期四''星期五''星期六'];
function getDay2({
  return days[(new Date()).getDay()];
}

当然上述只是一个非常简单的 🌰,大家在编码过程中只需要主要点,如有涉及类似场景,请用这种方式去编码,体验更愉悦!


六、总结

策略设计模式让各种算法的代码、内部数据和依赖关系与其他代码隔离开来。不同客户端可通过一个简单接口执行算法,并能在运行时进行切换。当然设计实现功能的时候,如果需要使用策略设计模式,也更需要我们的工程师有一个功能全局把控的能力,以此也能凸显“新生代”的不同!



推荐阅读