重構 - 用各種方式優化自己的函數庫
來自專欄猿論
有時候,我會想:比我優秀的人,比我更努力。我努力有什麼用。但是現在我習慣反過來想這句話,別人為什麼會比我優秀,就是因為別人比我更努力。與其拼天賦,更不如比行動。
1.前言
最近有幾天時間空閑,也是在學怎麼寫更有可讀性的代碼,更簡單,方便的API。簡單來說就是重構方面的內容。今天簡單分享下,對以前一個小項目(ecDo,歡迎大家star)的API重構方式,下面的的代碼如無說明,都是選取自我的項目中這個文件:ec-do-3.0.0-beta.1.js 中的 ecDo 這個對象(針對不同的重構目的,只列舉1-3個代表實例,不一一列出)。如果大家有什麼更好的方式,也歡迎在評論區留下您的建議。
首先說明一點,重構大家不要為重構而重構,要有目的重構。下面的改動,都是針對我原來的實現方式,更換更好的實現方式。主要會涉及在日常開發上,頻繁使用的三個設計原則(單一職責原則,開放-封閉原則,最少知識原則),關於API設計的原則,不止三個。還有里式替換原則,依賴倒置原則等,但是這幾個日常開發上沒有感覺出來,所以這裡就不多說了。 然後就是,雖然這幾個帶有『原則』的字樣,但是這些原則只是一個建議,指導的作用,沒有哪個原則是必須要遵守的,在開發上,是否應該,需要遵守這些原則,具體情況,具體分析。
2.單一職責原則
這部分內容,主要就是有些函數,違反了單一職責原則。這樣潛在的問題,可能會造成函數巨大,邏輯混亂,導致代碼難以維護等。
2-1.getCount
在以前的版本,對這個函數的定義是:返回數組(字元串)出現最多的幾次元素和出現次數。
原來實現的方案
/** * @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);},
調用方式
//返回值: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}]
這樣目前是沒有問題,但是這個函數承擔了三個職責。統計次數,處理長度,排序方式。而且,處理長度和排序方式,有其他的原生處理方式,在這裡寫感覺有些雞肋。
所以,重構這個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;},
3.開放-封閉原則
3-1.checkType
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 /^(0d{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 /^(0d{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));//falseecDo.checkType.addRule(password,function (str) { return /^[-a-zA-Z0-9._]+$/.test(str);})console.log(ecDo.checkType.check(***asdasd654zxc,password));//false
調用麻煩了一些,但是擴展性有了,以後面對新的需求可以更靈活的處理。
4.最少知識原則
最少知識原則,官方一點的解釋是:一個對象應當對其他對象有儘可能少的了解。在下面表現為:儘可能的讓用戶更簡單,更方便的使用相關的API。具體表現看下面的例子
4-1.trim
以前 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 多了,但是記憶成本和調用簡單了。
4-2.encryptStr
下面的 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+++++663ecDo.encryptStr(18819233362,[3,5,3],1,+)//result:+++19233+++ecDo.encryptStr(18819233362,[5],0)//result:*****233362ecDo.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+++++362console.log(`不加密字元 ${ecDo.encryptUnStr(18819233362,3,7,+)}`)//result:+++19233+++console.log(`字元串開始位置加密 ${ecDo.encryptStartStr(18819233362,4)}`)//result:****9233362console.log(`字元串結束位置加密 ${ecDo.encryptEndStr(18819233362,4)}`)//result:1881923****
結果一樣,但是調用就比之前簡單了,也不需要記憶太多東西。
類似4-1和4-2的改動還有幾個實例,在這裡就不列舉了!
4-3.cookie
這個實例與上面兩個實例不太一樣,上面兩個 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,只需要記一個。
5.代碼優化
5-1.checkPwdLevel
原來方案
/** * @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;},
這樣寫,處理的事情是一樣的,性能方面可以忽略不計,但是看著舒服。
5-2.upsetArr
原來方案
/** * @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;},
原理就是遍曆數組元素,然後將當前元素與以後隨機位置的元素進行交換,這樣亂序更加徹底。
6.小結
關於重構我自己的代碼庫,暫時就是這麼多了,這些實例只是部分,還是一些 API 因為重構的目的,實現方式都基本一樣,就不重複舉例了。需要的到 github (ec-do-3.0.0-beta.1.js)上面看就好,關於我重構的這個文件,現在也只是一個 demo ,測試的階段,以後還是繼續的改進。如果大家有什麼建議,或者需要增加什麼 API 歡迎在評論區瀏覽,大家多交流,多學習。
作者:守候i
鏈接:http://www.imooc.com/article/details/id/34976
來源:慕課網
本文原創發佈於慕課網 ,轉載請註明出處,謝謝合作
推薦閱讀:
【重磅】認證作者招募 | 打造個人品牌 so easy !
618福利空降來襲,拉開技術差距,就在此刻!
記一次Node項目的優化
關於RecyclerView你知道的不知道的都在這了(上)
原生ES-Module在瀏覽器中的嘗試
推薦閱讀:
※一些應用領域看到的問題
※Advanced SystemCare—一款當前最流行的電腦優化軟
※【優化演算法】一文搞懂RMSProp優化演算法
※尼康D7000或D7100優化校準與白平衡設置攝影指南