有时候,我会想:比我优秀的人,比我更努力。我努力有什么用。可是如今我习惯反过来想这句话,别人为何会比我优秀,就是由于别人比我更努力。与其拼天赋,更不如比行动。
最近有几天时间空闲,也是在学怎么写更有可读性的代码,更简单,方便的API。简单来讲就是重构方面的内容。今天简单分享下,对之前一个小项目(ecDo,欢迎你们star)的API重构方式,下面的的代码如无说明,都是选取自个人项目中这个文件:ec-do-3.0.0-beta.1.js 中的 ecDo 这个对象(针对不一样的重构目的,只列举1-3个表明实例,不一一列出)。若是你们有什么更好的方式,也欢迎在评论区留下您的建议。git
首先说明一点,重构你们不要为重构而重构,要有目的重构。下面的改动,都是针对我原来的实现方式,更换更好的实现方式。主要会涉及在平常开发上,频繁使用的三个设计原则(单一职责原则,开放-封闭原则,最少知识原则),关于API设计的原则,不止三个。还有里式替换原则,依赖倒置原则等,可是这几个平常开发上没有感受出来,因此这里就很少说了。 而后就是,虽然这几个带有‘原则’的字样,可是这些原则只是一个建议,指导的做用,没有哪一个原则是必需要遵照的,在开发上,是否应该,须要遵照这些原则,具体状况,具体分析。
这部份内容,主要就是有些函数,违反了单一职责原则。这样潜在的问题,可能会形成函数巨大,逻辑混乱,致使代码难以维护等。github
在之前的版本,对这个函数的定义是:返回数组(字符串)出现最多的几回元素和出现次数。数组
原来实现的方案微信
/** * @description 降序返回数组(字符串)每一个元素的出现次数 * @param arr 待处理数组 * @param rank 长度 (默认数组长度) * @param ranktype 排序方式(默认降序) */ getCount(arr, rank, ranktype) { let obj = {}, k, arr1 = [] //记录每一元素出现的次数 for (let i = 0, len = arr.length; i < len; i++) { k = arr[i]; if (obj[k]) { obj[k]++; } else { obj[k] = 1; } } //保存结果{el-'元素',count-出现次数} for (let o in obj) { arr1.push({el: o, count: obj[o]}); } //排序(降序) arr1.sort(function (n1, n2) { return n2.count - n1.count }); //若是ranktype为1,则为升序,反转数组 if (ranktype === 1) { arr1 = arr1.reverse(); } let rank1 = rank || arr1.length; return arr1.slice(0, rank1); },
调用方式cookie
//返回值:el->元素,count->次数 ecDo.getCount([1,2,3,1,2,5,2,4,1,2,6,2,1,3,2]) //默认状况,返回全部元素出现的次数 //result:[{"el":"2","count":6},{"el":"1","count":4},{"el":"3","count":2},{"el":"4","count":1},{"el":"5","count":1},{"el":"6","count":1}] ecDo.getCount([1,2,3,1,2,5,2,4,1,2,6,2,1,3,2],3) //传参(rank=3),只返回出现次数排序前三的 //result:[{"el":"2","count":6},{"el":"1","count":4},{"el":"3","count":2}] ecDo.getCount([1,2,3,1,2,5,2,4,1,2,6,2,1,3,2],null,1) //传参(ranktype=1,rank=null),升序返回全部元素出现次数 //result:[{"el":"6","count":1},{"el":"5","count":1},{"el":"4","count":1},{"el":"3","count":2},{"el":"1","count":4},{"el":"2","count":6}] ecDo.getCount([1,2,3,1,2,5,2,4,1,2,6,2,1,3,2],3,1) //传参(rank=3,ranktype=1),只返回出现次数排序(升序)前三的 //result:[{"el":"6","count":1},{"el":"5","count":1},{"el":"4","count":1}]
这样目前是没有问题,可是这个函数承担了三个职责。统计次数,处理长度,排序方式。并且,处理长度和排序方式,有其余的原生处理方式,在这里写感受有些鸡肋。dom
因此,重构这个API,就只保留统计次数这个职。至于长度和排序,有不少方式处理,slice,splice,length,sort等API或者属性均可以处理。函数
/** * @description 降序返回数组(字符串)每一个元素的出现次数 * @param arr * @return {Array} */ getCount(arr) { let obj = {}, k, arr1 = [] //记录每一元素出现的次数 for (let i = 0, len = arr.length; i < len; i++) { k = arr[i]; if (obj[k]) { obj[k]++; } else { obj[k] = 1; } } //保存结果{el-'元素',count-出现次数} for (let o in obj) { arr1.push({el: o, count: obj[o]}); } //排序(降序) arr1.sort(function (n1, n2) { return n2.count - n1.count }); return arr1; },
checkType 检测字符串类型。之前的实现方式是。性能
/** * @description 检测字符串 * @param str 待处理字符串 * @param type 待检测的类型 */ checkType(str, type) { switch (type) { case 'email': return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str); case 'mobile': return /^1[3|4|5|7|8][0-9]{9}$/.test(str); case 'tel': return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str); case 'number': return /^[0-9]$/.test(str); case 'english': return /^[a-zA-Z]+$/.test(str); case 'text': return /^\w+$/.test(str); case 'chinese': return /^[\u4E00-\u9FA5]+$/.test(str); case 'lower': return /^[a-z]+$/.test(str); case 'upper': return /^[A-Z]+$/.test(str); default: return true; } },
调用方式学习
ecDo.checkType('165226226326','mobile'); //result:false
由于 165226226326 不是一个有效的电话格式,因此返回false。可是这样会存在一个问题就是,若是之后我想加什么检测的规则呢?好比增长一个密码的规则。密码能够报错大小写字母,数字,点和下划线。上面的方案,就是只能在增长一个case。这样改违反了开放-封闭原则,并且这样会存在什么问题,我在以前讲策略模式的时候,已经说起,这里不重复。测试
因此个人作法就是,给它增长扩展性。
/** * @description 检测字符串 */ checkType:(function(){ let rules={ email(str){ return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str); }, mobile(str){ return /^1[3|4|5|7|8][0-9]{9}$/.test(str); }, tel(str){ return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str); }, number(str){ return /^[0-9]$/.test(str); }, english(str){ return /^[a-zA-Z]+$/.test(str); }, text(str){ return /^\w+$/.test(str); }, chinese(str){ return /^[\u4E00-\u9FA5]+$/.test(str); }, lower(str){ return /^[a-z]+$/.test(str); }, upper(str){ return /^[A-Z]+$/.test(str); } }; return { /** * @description 检测接口 * @param str 待处理字符串 * @param type 待检测的类型 */ check(str, type){ return rules[type]?rules[type](str):false; }, /** * @description 添加规则扩展接口 * @param type 规则名称 * @param fn 处理函数 */ addRule(type,fn){ rules[type]=fn; } } })(),
调用方式
console.log(ecDo.checkType.check('165226226326','mobile'));//false ecDo.checkType.addRule('password',function (str) { return /^[-a-zA-Z0-9._]+$/.test(str); }) console.log(ecDo.checkType.check('***asdasd654zxc','password'));//false
调用麻烦了一些,可是扩展性有了,之后面对新的需求能够更灵活的处理。
最少知识原则,官方一点的解释是:一个对象应当对其余对象有尽量少的了解。在下面表现为:尽量的让用户更简单,更方便的使用相关的API。具体表现看下面的例子
之前 trim 函数实现方式
/** * @description 大小写切换 * @param str 待处理字符串 * @param type 去除类型(1-全部空格 2-左右空格 3-左空格 4-右空格) */ trim(str, type) { switch (type) { case 1: return str.replace(/\s+/g, ""); case 2: return str.replace(/(^\s*)|(\s*$)/g, ""); case 3: return str.replace(/(^\s*)/g, ""); case 4: return str.replace(/(\s*$)/g, ""); default: return str; } }
调用方式
//去除全部空格 ecDo.trim(' 1235asd',1); //去除左空格 ecDo.trim(' 1235 asd ',3);
这样的方式存在有目共睹,表明 type 参数的1,2,3,4能够说是一个神仙数,虽然对于开发者而言,知道是什么。可是若是有其余人使用,那么这样的 API 就增长了记忆成本和调用的复杂性。
为了解决这个问题,处理方式就分拆 API 。
/** * @description 清除左右空格 */ trim(str) { return str.replace(/(^\s*)|(\s*$)/g, ""); }, /** * @description 清除全部空格 */ trimAll(str){ return str.replace(/\s+/g, ""); }, /** * @description 清除左空格 */ trimLeft(str){ return str.replace(/(^\s*)/g, ""); }, /** * @description 清除右空格 */ trimRight(str){ return str.replace(/(\s*$)/g, ""); }
调用方式
//去除全部空格 ecDo.trim(' 123 5asd'); //去除左空格 ecDo.trimLeft(' 1235 asd ');
这样 API 多了,可是记忆成本和调用简单了。
下面的 API 在简单使用方便,表现得更为突出
原来方案
/** * @description 加密字符串 * @param str 字符串 * @param regArr 字符格式 * @param type 替换方式 * @param ARepText 替换的字符(默认*) */ encryptStr(str, regArr, type = 0, ARepText = '*') { let regtext = '', Reg = null, replaceText = ARepText; //repeatStr是在上面定义过的(字符串循环复制),你们注意哦 if (regArr.length === 3 && type === 0) { regtext = '(\\w{' + regArr[0] + '})\\w{' + regArr[1] + '}(\\w{' + regArr[2] + '})' Reg = new RegExp(regtext); let replaceCount = this.repeatStr(replaceText, regArr[1]); return str.replace(Reg, '$1' + replaceCount + '$2') } else if (regArr.length === 3 && type === 1) { regtext = '\\w{' + regArr[0] + '}(\\w{' + regArr[1] + '})\\w{' + regArr[2] + '}' Reg = new RegExp(regtext); let replaceCount1 = this.repeatStr(replaceText, regArr[0]); let replaceCount2 = this.repeatStr(replaceText, regArr[2]); return str.replace(Reg, replaceCount1 + '$1' + replaceCount2) } else if (regArr.length === 1 && type === 0) { regtext = '(^\\w{' + regArr[0] + '})' Reg = new RegExp(regtext); let replaceCount = this.repeatStr(replaceText, regArr[0]); return str.replace(Reg, replaceCount) } else if (regArr.length === 1 && type === 1) { regtext = '(\\w{' + regArr[0] + '}$)' Reg = new RegExp(regtext); let replaceCount = this.repeatStr(replaceText, regArr[0]); return str.replace(Reg, replaceCount) } },
调用方式
ecDo.encryptStr('18819322663',[3,5,3],0,'+') //result:188+++++663 ecDo.encryptStr('18819233362',[3,5,3],1,'+') //result:+++19233+++ ecDo.encryptStr('18819233362',[5],0) //result:*****233362 ecDo.encryptStr('18819233362',[5],1) //result:"188192*****"
这个 API 存在的问题也是同样,太多的神仙数,好比[3,5,3],1,0等。相对于4-1的例子,这个对使用这形成的记忆成本和调用复杂性更大。甚至很容易会搞晕。若是是阅读源码,if-else的判断,别说是其余人了,就算是我这个开发者,我都会被搞蒙。
处理这些问题,也相似4-1。拆分 API 。
/** * @description 加密字符串 * @param regIndex 加密位置 (开始加密的索引,结束加密的索引) * @param ARepText 加密的字符 (默认*) */ encryptStr(str, regIndex, ARepText = '*') { let regtext = '', Reg = null, _regIndex=regIndex.split(','), replaceText = ARepText; //repeatStr是在上面定义过的(字符串循环复制),你们注意哦 _regIndex=_regIndex.map(item=>+item); regtext = '(\\w{' + _regIndex[0] + '})\\w{' + (1+_regIndex[1]-_regIndex[0]) + '}'; Reg = new RegExp(regtext); let replaceCount = this.repeatStr(replaceText, (1+_regIndex[1]-_regIndex[0])); return str.replace(Reg, '$1' + replaceCount); }, /** * @description 不加密字符串 * @param regIndex 不加密位置 (开始加密的索引,结束加密的索引) * @param ARepText 不加密的字符 (默认*) */ encryptUnStr(str, regIndex, ARepText = '*') { let regtext = '', Reg = null, _regIndex=regIndex.split(','), replaceText = ARepText; _regIndex=_regIndex.map(item=>+item); regtext = '(\\w{' + _regIndex[0] + '})(\\w{' + (1+_regIndex[1]-_regIndex[0]) + '})(\\w{' + (str.length-_regIndex[1]-1) + '})'; Reg = new RegExp(regtext); let replaceCount1 = this.repeatStr(replaceText, _regIndex[0]); let replaceCount2 = this.repeatStr(replaceText, str.length-_regIndex[1]-1); return str.replace(Reg, replaceCount1 + '$2' + replaceCount2); }, /** * @description 字符串开始位置加密 * @param regIndex 加密长度 * @param ARepText 加密的字符 (默认*) */ encryptStartStr(str,length,replaceText = '*'){ let regtext = '(\\w{' + length + '})'; let Reg = new RegExp(regtext); let replaceCount = this.repeatStr(replaceText, length); return str.replace(Reg, replaceCount); }, /** * @description 字符串结束位置加密 * @param regIndex 加密长度 * @param ARepText 加密的字符 (默认*) */ encryptEndStr(str,length,replaceText = '*'){ return this.encryptStartStr(str.split('').reverse().join(''),length,replaceText).split('').reverse().join(''); },
调用方式
console.log(`加密字符 ${ecDo.encryptStr('18819233362','3,7','+')}`) //result:188+++++362 console.log(`不加密字符 ${ecDo.encryptUnStr('18819233362','3,7','+')}`) //result:+++19233+++ console.log(`字符串开始位置加密 ${ecDo.encryptStartStr('18819233362','4')}`) //result:****9233362 console.log(`字符串结束位置加密 ${ecDo.encryptEndStr('18819233362','4')}`) //result:1881923****
结果同样,可是调用就比以前简单了,也不须要记忆太多东西。
相似4-1和4-2的改动还有几个实例,在这里就不列举了!
这个实例与上面两个实例不太同样,上面两个 API 为了简化使用,把一个 API 拆分红多个,可是这个 API 是把多个 API 合并成一个。
/** * @description 设置cookie * @param name cookie名称 * @param value 值 * @param iDay 有效时间(天数) */ setCookie(name, value, iDay) { let oDate = new Date(); oDate.setDate(oDate.getDate() + iDay); document.cookie = name + '=' + value + ';expires=' + oDate; }, /** * @description 获取cookie * @param name cookie名称 */ getCookie(name) { let arr = document.cookie.split('; '),arr2; for (let i = 0; i < arr.length; i++) { arr2 = arr[i].split('='); if (arr2[0] == name) { return arr2[1]; } } return ''; }, /** * @description 删除cookie * @param name cookie名称 */ removeCookie(name) { this.setCookie(name, 1, -1); },
调用方式
ecDo.setCookie(cookieName,'守候',1)//设置(有效时间为1天) ecDo.getCookie(cookieName)//获取 ecDo.removeCookie(cookieName)//删除
新增API
/** * @description 操做cookie * @param name cookie名称 * @param value 值 * @param iDay 有效时间(天数) */ cookie(name, value, iDay){ if(arguments.length===1){ return this.getCookie(name); } else{ this.setCookie(name, value, iDay); } },
调用方式
ecDo.cookie(cookieName,'守候',1)//设置 ecDo.cookie(cookieName)//获取 ecDo.cookie(cookieName,'守候',-1)//删除(中间的值没有意义了,只要cookie天数设置了-1,就会删除。)
这样调用,使用方法的记忆成本增长了,可是不须要记3个API,只须要记一个。
原来方案
/** * @description 检测密码强度 */ checkPwdLevel(str) { let nowLv = 0; if (str.length < 6) { return nowLv } if (/[0-9]/.test(str)) { nowLv++ } if (/[a-z]/.test(str)) { nowLv++ } if (/[A-Z]/.test(str)) { nowLv++ } if (/[\.|-|_]/.test(str)) { nowLv++ } return nowLv; },
调用方式
console.log(ecDo.checkPwdLevel('asd188AS19663362_.')); //4
这样写没问题,可是想必你们和我同样,看到if有点多,并且if为true的时候,作的事情仍是同样的,就忍不住要折腾了。就有了下面的方案。
/** * @description 检测密码强度 */ checkPwdLevel(str) { let nowLv = 0; if (str.length < 6) { return nowLv } //把规则整理成数组,再进行循环判断 let rules=[/[0-9]/,/[a-z]/,/[A-Z]/,/[\.|-|_]/]; for(let i=0;i<rules.length;i++){ if(rules[i].test(str)){ nowLv++; } } return nowLv; },
这样写,处理的事情是同样的,性能方面能够忽略不计,可是看着舒服。
原来方案
/** * @description 数组顺序打乱 * @param arr */ upsetArr(arr) { return arr.sort(() => { return Math.random() - 0.5 }); },
调用方式
ecDo.upsetArr([1,2,3,4,5,6,7,8,9]);
这种方式没错,可是有个遗憾的地方就是不能实现彻底乱序,就是乱的不够均匀。因此换了一种方式。
/** * @description 数组顺序打乱 * @param arr * @return {Array.<T>} */ upsetArr(arr) { let j,_item; for (let i=0; i<arr.length; i++) { j = Math.floor(Math.random() * i); _item = arr[i]; arr[i] = arr[j]; arr[j] = _item; } return arr; },
原理就是遍历数组元素,而后将当前元素与之后随机位置的元素进行交换,这样乱序更加完全。
关于重构我本身的代码库,暂时就是这么多了,这些实例只是部分,仍是一些 API 由于重构的目的,实现方式都基本同样,就不重复举例了。须要的到 github (ec-do-3.0.0-beta.1.js)上面看就好,关于我重构的这个文件,如今也只是一个 demo ,测试的阶段,之后仍是继续的改进。若是你们有什么建议,或者须要增长什么 API 欢迎在评论区浏览,你们多交流,多学习。
-------------------------华丽的分割线--------------------
想了解更多,关注关注个人微信公众号:守候书阁