likes
comments
collection
share

前端常用设计模式(一)

作者站长头像
站长
· 阅读数 42

策略模式

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

对于概念我们不必死记硬背,我们只需要记住策略模式解决了什么问题,以及能举出例子。

策略模式有效的解决if else逻辑复杂不可维护的情况

案例

年底了,大家辛苦了,老板给小明提了一个计算年终奖的需求:

年终奖规则

  • s等级是5倍工资
  • a等级3倍工资
  • b等级2倍工资
  • c等级1倍工资,

小明接到需求后,作为资深 if-else 侠,代码一把梭,三下五除二就写出一套功能完备的代码:

  /*
   *  salary 员工的工资
   *  level 年终等级
   */
  function yearEndAwards(salary, level) {
    if (level === "s") {
      return salary * 5;
    }
    if (level === "a") {
      return salary * 3;
    }
    if (level === "b") {
      return salary * 2;
    }
    if (level === "c") {
      return salary * 1;
    }
  }
  console.log("小明年终奖", yearEndAwards(1000, "s")); // 小明年终奖 5000
  console.log("小红年终奖", yearEndAwards(5000, "a")); // 小红年终奖 15000

第二天,老板又对小明说今年公司赚了钱,为了鼓励员工,s等级再送一个苹果手机,a等级再送一个电动牙刷 。于是小明就去把代码改了。

  function yearEndAwards(salary, level) {
    if (level === "s") {
      console.log("再送一个苹果手机");
      return salary * 5;
    }
    if (level === "a") {
      console.log("再送一个电动牙刷");
      return salary * 3;
    }
    if (level === "b") {
      return salary * 2;
    }
    if (level === "c") {
      return salary * 1;
    }
  }

老板第三天又说,不能让摸鱼的人也得到了年终奖,于是又设置了一个等级d,没有年终奖,于是小明又去加一个if else判断。

  function yearEndAwards(salary, level) {
    if (level === "s") {
      console.log("再送一个苹果手机");
      return salary * 5;
    }
    else if (level === "a") {
      console.log("再送一个电动牙刷");
      return salary * 3;
    }
    else if (level === "b") {
      return salary * 2;
    }
    else if (level === "c") {
      return salary * 1;
    }
    // 新增d等级,没有年终奖
    else if (level === "d") {
      return 0;
    }
  }

上述代码运行起来确实没啥毛病。但也只是“运行起来”没毛病而已。它有以下问题:

  • 首先,它违背了“单一功能”原则。一个 function 里面,它竟然处理了5坨逻辑——这个函数的逻辑太胖了!这样会带来什么样的糟糕后果,比如说万一其中一行代码出了 Bug,那么整个计算年终奖逻辑都会崩坏;与此同时出现了Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用,比如我有一个另外模块也要计算s等级的年终奖,我就只能手动把s逻辑的代码复制粘贴过去,后面s年终奖新增了苹果手机奖励,这时又需要把代码重新复制一遍过去。总之,见到胖逻辑,我们的第一反应,就是一个字——拆!
  • 不仅如此,它还违背了“开放封闭”原则(对扩展开放,对修改封闭)。假如有一天老板再次找到小明,要他加一个新的年终奖等级怎么办?他只能继续在yearEndAwards函数里增加if,else。

小明羊了,这时我接手了代码,用策略模式改造了一下:

  // 策略对象
  let strategysObj = {
    s(salary) {
      console.log("再送一个苹果手机");
      return salary * 5;
    },
    a(salary) {
      console.log("再送一个电动牙刷");
      return salary * 3;
    },
    b(salary) {
      return salary * 2;
    },
    c(salary) {
      return salary * 1;
    },
    d(salary) {
      return salary * 0;
    },
  };
  function yearEndAwards(salary, level) {
    return strategysObj[level](salary);
  }
  console.log("小明年终奖", yearEndAwards(1000, "s"));
  console.log("小红年终奖", yearEndAwards(5000, "a"));

这时候如果你需要一个新的年终奖等级e,只需要给 strategysObj 新增一个映射关系即可:

strategysObj.e = (salary) => {
  return salary * 0.1;
}

这样也符合开放封闭原则,对扩展开放,对修改是封闭

最后,我们再来看策略模式的定义

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

算法,就是我们这个场景中的计算年终奖的逻辑,它也可以是你任何一个功能函数的逻辑;“封装”就是把某一功能点对应的逻辑给提出来;“可替换”建立在封装的基础上,只是说这个“替换”的判断过程,咱们不能直接怼 if-else,而要考虑更优的映射方案。

给大家加个彩蛋

第七天老板又对我说,研发部天天加班,辛苦了。每个部门贡献不一样,需要根据部门来设置年终奖的奖励系数,于是需求变成了这样

  • 研发部 s等级 6倍年终 ,a等级 4倍年终, b等级 3倍年终
  • 设计部 s等级 4倍年终 ,a等级 3倍年终, b等级 2倍年终
  • 人事部 s等级 2倍年终 ,a等级 1倍年终, b等级 0.5倍年终

这时,增加了一个部门的参数,这时可以考虑map对象来做映射。

  const strategysMap = new Map([
    // 研发部门映射
    [
      { level: "s", department: "研发部" },
      (salary) => {
        return salary * 6;
      },
    ],
    [
      { level: "a", department: "研发部" },
      (salary) => {
        return salary * 4;
      },
    ],
    [
      { level: "b", department: "研发部" },
      (salary) => {
        return salary * 3;
      },
    ],
    // 设计部门映射
    [
      { level: "s", department: "设计部" },
      (salary) => {
        return salary * 4;
      },
    ],
    [
      { level: "a", department: "设计部" },
      (salary) => {
        return salary * 3;
      },
    ],
    [
      { level: "b", department: "设计部" },
      (salary) => {
        return salary * 2;
      },
    ],
    // 人事部门映射
    [
      { level: "s", department: "人事部" },
      (salary) => {
        return salary * 2;
      },
    ],
    [
      { level: "a", department: "人事部" },
      (salary) => {
        return salary * 1;
      },
    ],
    [
      { level: "b", department: "人事部" },
      (salary) => {
        return salary * 0.5;
      },
    ],
  ]);
    /*/**
   * level      年终等级
   * department 部门
   * salary      工资
   */
  function yearEndAwards(level, department, salary) {
    let money = 0; // 最终年终奖
    let res = [...strategysMap].filter(([key, value]) => {
      return key.level === level && key.department === department;
    });
    res.forEach(([key, value]) => {
      money = value(salary);
    });
    return money;
  }
  console.log("小红年终奖", yearEndAwards("a", "人事部", 2000)); //  小红年终奖 2000
  console.log("小明年终奖", yearEndAwards("s", "研发部", 4000)); // 小明年终奖 24000
  console.log("小刚年终奖", yearEndAwards("b", "设计部", 3000)); // 小刚年终奖 6000

适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。

生活中的适配器

现实生活中,很多手机都取消了3.5mm的耳机孔,我们就想用这种类型的耳机,就可以买一个耳机转换器满足我们的需求。如下图 前端常用设计模式(一)

再比如直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。

前端常用设计模式(一)

上面这些实际接口与目标接口不匹配的尴尬就可以用一个叫适配器的东西来化解

真实业务场景

这里直接参考修言大佬的例子

  export default class HttpUtils {
    // get方法
    static get(url) {
      return new Promise((resolve, reject) => {
        // 调用fetch
        fetch(url)
          .then(response => response.json())
          .then(result => {
            resolve(result)
          })
          .catch(error => {
            reject(error)
          })
      })
    }
    // post方法,data以object形式传入
    static post(url, data) {
      return new Promise((resolve, reject) => {
        // 调用fetch
        fetch(url, {
          method: 'POST',
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          // 将object类型的数据格式化为合法的body参数
          body: this.changeData(data)
        })
          .then(response => response.json())
          .then(result => {
            resolve(result)
          })
          .catch(error => {
            reject(error)
          })
      })
    }
    // body请求体的格式化方法
    static changeData(obj) {
      var prop,
        str = ''
      var i = 0
      for (prop in obj) {
        if (!prop) {
          return
        }
        if (i == 0) {
          str += prop + '=' + obj[prop]
        } else {
          str += '&' + prop + '=' + obj[prop]
        }
        i++
      }
      return str
    }
  }

当想使用fetch 发起请求时,只需要按如下方式调用即可:

  // 定义目标url地址
  const URL = "xxxxx"
  // 定义post入参
  const params = {
      ...
  }
  // 发起post请求
   const postResponse = await HttpUtils.post(URL,params) || {}
   // 发起get请求
   const getResponse = await HttpUtils.get(URL)

但项目中不免有些旧的接口调用方式,比如有如下的:

// 发送get请求
Ajax('get', url地址, post入参, function(data){
    // 成功的回调逻辑
}, function(error){
    // 失败的回调逻辑
})

为了抹平差异,可以采用适配器模式

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
         // 实际的请求全部由新接口发起
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // 捕捉网络错误
        if(failed){
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}

如此一来,我们只需要编写一个适配器函数AjaxAdapter,并用适配器去承接旧接口的参数,就可以实现新旧接口的无缝衔接了~

最后,推荐修言的小册,[JavaScript 设计模式核⼼原理与应⽤实践],本文大部分参考此小册。(juejin.cn/book/684473…)

前端常用设计模式(二)讲解发布订阅模式和迭代器模式 写作中...

转载自:https://juejin.cn/post/7182750482438291512
评论
请登录