重構 - 用各種方式優化自己的函式庫

守候i發表於2018-06-11

有時候,我會想:比我優秀的人,比我更努力。我努力有什麼用。但是現在我習慣反過來想這句話,別人為什麼會比我優秀,就是因為別人比我更努力。與其拼天賦,更不如比行動。

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 /^(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
複製程式碼

呼叫麻煩了一些,但是擴充套件性有了,以後面對新的需求可以更靈活的處理。

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+++++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的改動還有幾個例項,在這裡就不列舉了!

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 歡迎在評論區瀏覽,大家多交流,多學習。

-------------------------華麗的分割線--------------------

想了解更多,關注關注我的微信公眾號:守候書閣

重構 - 用各種方式優化自己的函式庫

相關文章