5分钟即可掌握的前端高效利器:JavaScript策略模式

开发 前端
策略模式是一种简单却常用的设计模式,它的应用场景非常广泛。我们先了解下策略模式的概念,再通过代码示例来更清晰的认识它。

 [[341697]]

浅谈 JavaScript 中策略模式的使用:

  •  什么是设计模式
  •  什么是策略模式
  •  策略模式在 JavaScript 中的应用(使用策略模式封装百度AI识别调用)
  •  策略模式在 Vue 组件封装中的应用(使用策略模式封装Select组件)

什么是设计模式

设想有一个电子爱好者,虽然他没有经过正规的培训,但是却日积月累地设计并制造出了许多有用的电子设备:业余无线电、盖革计数器、报警器等。有一天这个爱好者决定重新回到学校去攻读电子学学位,来让自己的才能得到正式的认可。随着课程的展开,这个爱好者突然发现课程内容都似曾相识。似曾相识的不是术语或表述的方式,而是背后的概念。这个爱好者不断学到一些名称和原理,虽然这些名称和原理原来他并不知道,但事实上他多年以来一直都在使用。整个过程只不过是一个接一个的顿悟。

设计模式沉思录 ,John Vlissides, 第一章 1.2节

我们在写代码的时候,一定也遇到过许多类似的场景。随着经验的增加,我们对于这些常见场景的处理越来越得心应手,甚至总结出了针对性的“套路”,下次遇到此类问题直接运用“套路”解决,省心又省力。这些在软件开发过程中逐渐积累下来的“套路”就是设计模式。

设计模式的目标之一就是提高代码的可复用性、可扩展性和可维护性。正因如此,虽然有时候我们不知道某个设计模式,但是看了相关书籍或文章后会有一种“啊,原来这就是设计模式”的恍然大明白。

如果你看完这篇文章后也有此感觉,那么恭喜你,你已经在高效程序员的道路上一路狂奔了。

什么是策略模式

策略模式是一种简单却常用的设计模式,它的应用场景非常广泛。我们先了解下策略模式的概念,再通过代码示例来更清晰的认识它。

策略模式由两部分构成:一部分是封装不同策略的策略组,另一部分是 Context。通过组合和委托来让 Context 拥有执行策略的能力,从而实现可复用、可扩展和可维护,并且避免大量复制粘贴的工作。

策略模式的典型应用场景是表单校验中,对于校验规则的封装。接下来我们就通过一个简单的例子具体了解一下:

粗糙的表单校验

一个常见的登录表单代码如下: 

  1. <form id='login-form' action="" method="post">  
  2.     <label for="account">手机号</label>  
  3.     <input type="number" id="account" name="account">  
  4.     <label for="password">密码</label>  
  5.     <input type="password" id="password" name="password">  
  6.     <button id='login'>登录</button>  
  7. </form>  
  8. <script> 
  9.      var loginForm = document.getElementById('login-form'); 
  10.     loginForm.onsubmit = function (e) { 
  11.         e.preventDefault();    
  12.         var account = document.getElementById("account").value;  
  13.         var pwd = document.getElementById("password").value;  
  14.         if(account===null||account===''){  
  15.             alert('手机号不能为空');  
  16.             return false;  
  17.         }  
  18.         if(pwd===null||pwd===''){  
  19.             alert('密码不能为空');  
  20.             return false;  
  21.         }  
  22.         if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) {  
  23.             alert('手机号格式错误');  
  24.             return false;  
  25.         }  
  26.         if(pwd.length<6){  
  27.             alert('密码不能小于六位');  
  28.             return false;  
  29.         }  
  30.         // ajax 发送请求  
  31.     }  
  32. </script> 

以上代码,虽然功能没问题,但是缺点也很明显:

代码里遍地都是 if 语句,并且它们缺乏弹性:每新增一种、或者修改原有校验规则,我们都必须去改loginForm.onsubmit内部的代码。另外逻辑的复用性也很差:如果有其它表单也是用同样的规则,这段代码并不能复用,只能复制。当校验规则发生变化时,比如上文的正则校验并不能匹配虚拟运营商14/17号段,我们就需要手动同步多处代码变更(Ctrl+C/Ctrl+V)。

优秀的表单验证

接下来我们通过策略模式的思路改写一下上段代码,首先抽离并封装校验逻辑为策略组: 

  1. var strategies = {  
  2.     isNonEmpty: function (value, errorMsg) {  
  3.         if (value === '' || value === null) {  
  4.             return errorMsg;  
  5.         }  
  6.     },  
  7.     isMobile: function (value, errorMsg) { // 手机号码格式  
  8.         if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) {  
  9.             return errorMsg;  
  10.         }  
  11.     },  
  12.     minLength: function (value, length, errorMsg) {  
  13.         if (value.length < length) {  
  14.             return errorMsg;  
  15.         }  
  16.     }  
  17. }; 

接下来修改 Context: 

  1. var loginForm = document.getElementById('login-form');  
  2. loginForm.onsubmit = function (e) {  
  3.     e.preventDefault();   
  4.     var accountIsMobile = strategies.isMobile(account,'手机号格式错误');  
  5.     var pwdMinLength = strategies.minLength(pwd,8,'密码不能小于8位');  
  6.     var errorMsg = accountIsMobile||pwdMinLength;   
  7.     if(errorMsg){  
  8.         alert(errorMsg); 
  9.          return false;  
  10.     } 

对比两种实现,我们可以看到:分离了校验逻辑的代码如果需要扩展校验类型,在策略组中新增定义即可使用;如果需要修改某个校验的实现,直接修改相应策略即可全局生效。对于开发和维护都有明显的效率提升。

扩展:史诗的表单校验

有兴趣的朋友可以了解下 async-validator ,element-ui 和 antd 的表单校验都是基于 async-validator 封装的,可以说是史诗级别的表单校验了

通过表单校验的对比,相信大家都对策略模式有所了解,那么接下来通过两个例子具体了解下 JavaScript 中策略模式的应用:

使用策略模式调用百度AI图像识别

因为百度AI图像识别的接口类型不同,所需的参数格式也不尽相同。然而图像的压缩及上传、错误处理等部分是公用的。所以可以采用策略模式封装:

定义策略组

通过定义策略组来封装不同的接口及其参数:比如身份证识别接口的side字段,自定义识别的templateSign字段,以及行驶证识别的接收参数为poparamstData。 

  1. /**  
  2.  * 策略组  
  3.  * IDCARD:身份证识别  
  4.  * CUSTOMIZED:自定义识别  
  5.  * VL:行驶证识别  
  6.  */  
  7. var strategies = {  
  8.     IDCARD: function (base64) {  
  9.         return {  
  10.             path: 'idcard',  
  11.             param: {  
  12.                 'side': 'front',  
  13.                 'base64': base64  
  14.             }  
  15.         };  
  16.     },  
  17.     CUSTOMIZED: function (base64) {  
  18.         return {  
  19.             path: 'customized',  
  20.             param: {  
  21.                 'templateSign': '52cc2d402155xxxx',  
  22.                 'base64': base64  
  23.             }  
  24.         };  
  25.     },  
  26.     VL: function (base64) {  
  27.         return {  
  28.             path: 'vehicled',  
  29.             poparamstData: {  
  30.                 'base64': base64  
  31.             }  
  32.         };  
  33.     },  
  34. }; 

定义 Context 

  1. var ImageReader = function () { };  
  2. /**  
  3.  * 读取图像,调用接口,获取识别结果  
  4.  *   
  5.  * @param {*} type 待识别文件类型  
  6.  * @param {*} base64 待识别文件 BASE64码  
  7.  * @param {*} callBack 识别结果回调  
  8.  */  
  9. ImageReader.prototype.getOcrResult = function (type, base64, callBack) {  
  10.     let fileSize = (base64.length / (1024 * 1024)).toFixed(2);  
  11.     let compressedBase64 = '' 
  12.     let image = new Image();  
  13.     image.src = base64 
  14.     image.onload = function () {  
  15.         /**  
  16.          * 图片压缩处理及异常处理,代码略  
  17.          */     
  18.         let postData = strategies[type](compressedBase64); 
  19.         ajax( 
  20.             host + postData.path, {  
  21.                 data: postData.param,  
  22.                 type: 'POST',  
  23.                 headers: {  
  24.                     'Content-Type': 'application/x-www-form-urlencoded'  
  25.                 },  
  26.                 success: function (res) {  
  27.                     var data = JSON.parse(res);  
  28.                     // 暴露给 UI 层的统一的错误码  
  29.                     if (data.error_code !== undefined && data.error_code !== 0) {  
  30.                         var errorData = {  
  31.                             error: 1,  
  32.                             title: '错误 ' + data.error_code,  
  33.                             content: 'error message'  
  34.                         };  
  35.                         callBack(errorData);  
  36.                     } else {  
  37.                         callBack(data);  
  38.                     }  
  39.                 }  
  40.             });  
  41.     };  
  42. }; 

调用方式 

  1. var imageReader = new ImageReader();  
  2. imageReader.getOcrResult('IDCARD', this.result.toString(), callback); 

使用策略模式封装 Vue Select 组件

某项目中多处用到了 element-ui 的 select 组件,其内在逻辑类似,都是初始化时获取下拉列表的数据源,然后在选中某一项时 dispatch 不同的 action。遂考虑使用策略模式封装。

Context

在本例中,组件向外部暴露一个 prop,调用方指定该 prop 从而加载不同的策略。那么定义 Context 如下: 

  1. <template>  
  2.   <el-select v-model="selectedValue" placeholder="请选择" @change="optionChanged" size="mini" clearable>  
  3.     <el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">  
  4.     </el-option>  
  5.   </el-select>  
  6. </template>  
  1. data() {  
  2.     return {  
  3.       selectedValue: undefined,  
  4.       options: [],  
  5.       action: "",  
  6.     };  
  7.   },  
  8.   props: {  
  9.     // 暴露给外部的 select-type  
  10.     selectType: {  
  11.       type: String  
  12.     },  
  13.   },  
  14.   created() {  
  15.    // 获取 options  
  16.    this.valuation();  
  17.   },  
  18.     methods: {  
  19.     optionChanged() {  
  20.       this.$emit(this.action, this.selectedValue);  
  21.     },  
  22.     setOptions(option) {  
  23.       this.$store.dispatch(this.action, option);  
  24.     },  
  25.     valuation() {  
  26.       // 获取 options 数据  
  27.     }  
  28.   }, 

外部通过如下方式调用组件:

  1. <MySelect selectType="product"/> 

strategies

然后定义策略组: 

  1. let strategies = {  
  2.     source: {  
  3.         action: "sourceOption",  
  4.         getOptions:  function() {  
  5.             // 拉取 options  
  6.         }  
  7.     },  
  8.     product: {  
  9.         action: "productOption",  
  10.         getOptions:  function() {  
  11.             // 拉取 options  
  12.         }  
  13.     },  
  14.     ...  

异步

至此该组件的基本结构已经清晰,但还存在一个问题:组件加载时是异步拉取的 options, 而页面初始化的时候很可能 options 还没有返回,导致 select 的 options 仍为空。所以此处应该修改代码,同步获取 options: 

  1. // 策略组修改  
  2. source: {  
  3.     action: "sourceOption",  
  4.     getOptions: async function() {  
  5.         // await 拉取 options  
  6.     }  
  7.   },  
  8. // 组件修改  
  9. methods: {  
  10.     ...  
  11.     async valuation() {  
  12.         ...  
  13.     }  

继续优化

但我们不是每次加载组件都需要拉取 options,如果这些 options 在其他组件或者页面也被使用到,那么可以考虑将其存入 vuex 中。

最开始的思路是高阶组件,即定义一个包装后的select模板,通过高阶组件的方式扩展其数据源与action(变化的部分)然而这个思路不是那么的vue(主要是slots不太好处理) 于是考虑策略模式改写该组件

总结

通过以上两个例子,我们可以看到:

  •  策略模式符合开放-封闭原则
  •  如果代码里需要写大量的if-else语句,那么考虑使用策略模式
  •  如果多个组件(类)之间的区别仅在于它们的行为,考虑采用策略模式 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-06-07 09:51:22

原型模式序列化

2020-05-15 07:30:08

黑客Thunderbolt漏洞

2019-07-24 15:29:55

JavaScript开发 技巧

2020-12-17 10:00:16

Python协程线程

2021-01-29 11:25:57

Python爬山算法函数优化

2021-03-12 09:45:00

Python关联规则算法

2019-12-23 16:42:44

JavaScript前端开发

2020-12-07 11:23:32

Scrapy爬虫Python

2021-03-23 15:35:36

Adam优化语言

2017-01-10 09:07:53

tcpdumpGET请求

2020-10-27 10:43:24

Redis字符串数据库

2018-01-30 05:04:06

2020-12-01 12:44:44

PythonHook钩子函数

2020-11-24 11:50:52

Python文件代码

2021-04-19 23:29:44

MakefilemacOSLinux

2009-11-17 14:50:50

Oracle调优

2021-01-11 09:33:37

Maven数目项目

2020-11-10 16:01:25

程序员设计模式技术

2021-04-27 10:16:51

优化机器学习人工智能

2012-06-28 10:26:51

Silverlight
点赞
收藏

51CTO技术栈公众号