查看: 4949|回復: 0

[JavaScript/JQuery] 重構 - 改善代碼的各方面問題

發表于 2018-5-12 08:00:02

重構不是對以前代碼的全盤否定,而是利用更好的方式,寫出更好,更有維護性代碼。不斷的追求與學習,才有更多的進步。

1.前言

做前端開發有一段時間了,在這段時間里面,對于自己的要求,不僅僅是項目能完成,功能正常使用這一層面上。還盡力的研究怎么寫出優雅的代碼,性能更好,維護性更強的代碼,通俗一點就是重構。這篇文章算是我一個小記錄,在此分享一下。該文章主要針對介紹,例子也簡單,深入復雜的例子等以后有適合的實例再進行寫作分享。如果大家對怎么寫出優雅的代碼,可維護的代碼,有自己的見解,或者有什么重構的實力,歡迎指點評論。

關于重構,準備寫一個系列的文章,不定時更新,主要針對以下方案:邏輯混亂重構,分離職責重構,添加擴展性重構,簡化使用重構,代碼復用重構。其中會穿插以下原則:單一職責原則,最少知識原則,開放-封閉原則。如果大家對重構有什么好的想法,或者有什么好的實例,歡迎留言評論,留下寶貴的建議。

2.什么是重構

首先,重構不是重寫。重構大概的意思是在不影響項目的功能使用前提下,使用一系列的重構方式,改變項目的內部結構。提高項目內部的可讀性,可維護性。

無論是什么項目,都有一個從簡單到復雜的一個迭代過程。在這個過程里面,在不影響項目的使用情況下,需要不斷的對代碼進行優化,保持或者增加代碼的可讀性,可維護性。這樣一來,就可以避免在團隊協作開發上需要大量的溝通,交流。才能加入項目的開發中。

3.為什么重構

衣服臟了就洗,破了就補,不合穿就扔。

隨著業務需求的不斷增加,變更,舍棄,項目的代碼也難免會出現瑕疵,這就會影響代碼的可讀性,可維護性,甚至影響項目的性能。而重構的目的,就是為了解決這些瑕疵,保證代碼質量和性能。但是前提是不能影響項目的使用。

至于重構的原因,自己總結了一下,大概有以下幾點

函數邏輯結構混亂,或因為沒注釋原因,連原代碼寫作者都很難理清當中的邏輯。

函數無擴展性可言,遇到新的變化,
不能靈活的處理。

因為對象強耦合或者業務邏輯的原因,導致業務邏輯的代碼巨 大,維護的時候排查困難。

重復代碼太多,沒有復用性。

隨著技術的發展,代碼可能也需要使用新特性進行修改。

隨著學習的深入,對于以前的代碼,是否有著更好的一個解決方案。

因為代碼的寫法,雖然功能正常使用,但是性能消耗較多,需要換方案進行優化 4.何時重構

在合適的時間,在合適的事情

在我的理解中,重構可以說是貫穿整一個項目的開發和維護周期,可以當作重構就是開發的一部分。通俗講,在開發的任何時候,只要看到代碼有別扭,激發了強迫癥,就可以考慮重構了。只是,重構之前先參考下面幾點。

-首先,重構是需要花時間去做的一件事。花的時間可能比之前的開發時間還要多。

其次,重構是為了把代碼優化,前提是不能影響項目的使用。

最后,重構的難度大小不一,可能只是稍微改動,可能難度比之前開發還要難。

基于上面的幾點,需要大家去評估是否要進行重構。評估的指標,可以參考下面幾點

-數量: 需要重構的代碼是否過多。

質量: 可讀性,可維護性,代碼邏輯復雜度,等問題,對代碼的質量影響是否到了一個難以忍受的地步。

時間: 是否有充裕的時間進行重構和測試。

效果: 如果重構了代碼,得到哪些改善,比如代碼質量提高了,性能提升了,更好的支持后續功能等。 5.怎么重構

選定目標,針對性出擊

怎么重構,這個就是具體情況,具體分析了。如同“為什么重構一樣”。發現代碼有什么問題就針對什么情況進行改進。

重構也是寫代碼,但是不止于寫,更在于整理和優化。如果說寫代碼需要一個‘學習--了解-熟練’的過程,那么重構就需要一個‘學習-感悟-突破-熟練’的過程。

針對重構的情況,下面簡單的用幾個例子進行說明

5-1.函數無擴展性

如下面一個例子,在我一個庫的其中一個 API

  1. //檢測字符串
  2. //checkType('165226226326','mobile')
  3. //result:false
  4. let checkType=function(str, type) {
  5. switch (type) {
  6. case 'email':
  7. return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
  8. case 'mobile':
  9. return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
  10. case 'tel':
  11. return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
  12. case 'number':
  13. return /^[0-9]$/.test(str);
  14. case 'english':
  15. return /^[a-zA-Z]+$/.test(str);
  16. case 'text':
  17. return /^\w+$/.test(str);
  18. case 'chinese':
  19. return /^[\u4E00-\u9FA5]+$/.test(str);
  20. case 'lower':
  21. return /^[a-z]+$/.test(str);
  22. case 'upper':
  23. return /^[A-Z]+$/.test(str);
  24. default:
  25. return true;
  26. }
  27. }
復制代碼

這個 API 看著沒什么毛病,能檢測常用的一些數據。但是有以下兩個問題。

1.但是如果想到添加其他規則的呢?就得在函數里面增加 case 。添加一個規則就修改一次!這樣違反了開放-封閉原則(對擴展開放,對修改關閉)。而且這樣也會導致整個 API 變得臃腫,難維護。

2.還有一個問題就是,比如A頁面需要添加一個金額的校驗,B頁面需要一個日期的校驗,但是金額的校驗只在A頁面需要,日期的校驗只在B頁面需要。如果一直添加 case 。就是導致A頁面把只在B頁面需要的校驗規則也添加進去,造成不必要的開銷。B頁面也同理。

建議的方式是給這個 API 增加一個擴展的接口

  1. let checkType=(function(){
  2. let rules={
  3. email(str){
  4. return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
  5. },
  6. mobile(str){
  7. return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
  8. },
  9. tel(str){
  10. return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
  11. },
  12. number(str){
  13. return /^[0-9]$/.test(str);
  14. },
  15. english(str){
  16. return /^[a-zA-Z]+$/.test(str);
  17. },
  18. text(str){
  19. return /^\w+$/.test(str);
  20. },
  21. chinese(str){
  22. return /^[\u4E00-\u9FA5]+$/.test(str);
  23. },
  24. lower(str){
  25. return /^[a-z]+$/.test(str);
  26. },
  27. upper(str){
  28. return /^[A-Z]+$/.test(str);
  29. }
  30. };
  31. //暴露接口
  32. return {
  33. //校驗
  34. check(str, type){
  35. return rules[type]?rules[type](str):false;
  36. },
  37. //添加規則
  38. addRule(type,fn){
  39. rules[type]=fn;
  40. }
  41. }
  42. })();
  43. //調用方式
  44. //使用mobile校驗規則
  45. console.log(checkType.check('188170239','mobile'));
  46. //添加金額校驗規則
  47. checkType.addRule('money',function (str) {
  48. return /^[0-9]+(.[0-9]{2})?$/.test(str)
  49. });
  50. //使用金額校驗規則
  51. console.log(checkType.check('18.36','money'));
復制代碼

上面的代碼,是多了一些,但是理解起來也沒怎么費勁,而且拓展性也有了。

上面這個改進其實是使用了策略模式(把一系列的算法進行封裝,使算法代碼和邏輯代碼可以相互獨立,并且不會影響算法的使用)進行改進的。策略模式的概念理解起來有點繞,但是大家看著代碼,應該不繞。

這里展開講一點,在功能上來說,通過重構,給函數增加擴展性,這里實現了。但是如果上面的 checkType是一個開源項目的 API ,重構之前調用方式是:checkType('165226226326','phone') 。重構之后調用方式是:checkType.check('188170239','phone') ;或者 checkType.addRule() ;。如果開源項目的作者按照上面的方式重構,那么之前使用了開源項目的 checkType 這個 API 的開發者,就可能悲劇了,因為只要開發者一更新這個項目版本,就有問題。因為上面的重構沒有做向下兼容。

如果要向下兼容,其實也不難。加一個判斷而已。

  1. let checkType=(function(){
  2. let rules={
  3. email(str){
  4. return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
  5. },
  6. mobile(str){
  7. return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
  8. },
  9. tel(str){
  10. return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
  11. },
  12. number(str){
  13. return /^[0-9]$/.test(str);
  14. },
  15. english(str){
  16. return /^[a-zA-Z]+$/.test(str);
  17. },
  18. text(str){
  19. return /^\w+$/.test(str);
  20. },
  21. chinese(str){
  22. return /^[\u4E00-\u9FA5]+$/.test(str);
  23. },
  24. lower(str){
  25. return /^[a-z]+$/.test(str);
  26. },
  27. upper(str){
  28. return /^[A-Z]+$/.test(str);
  29. }
  30. };
  31. //暴露接口
  32. return function (str,type){
  33. //如果type是函數,就擴展rules,否則就是驗證數據
  34. if(type.constructor===Function){
  35. rules[str]=type;
  36. }
  37. else{
  38. return rules[type]?rules[type](str):false;
  39. }
  40. }
  41. })();
  42. console.log(checkType('188170239','mobile'));
  43. checkType('money',function (str) {
  44. return /^[0-9]+(.[0-9]{2})?$/.test(str)
  45. });
  46. //使用金額校驗規則
  47. console.log(checkType('18.36','money'));
復制代碼

這樣運行能正常,也有擴展性性,但是對于代碼潔癖的來說,這樣寫法不優雅。因為 checkType 違反了函數單一原則。一個函數負責過多的職責可能會導致以后不可估量的問題。

面對這樣的情況,就個人而言,了解的做法是:保留 checkType ,不做任何修改,在項目里面增加一個新的 API ,比如 checkTypOfString ,把重構的代碼寫到 checkTypOfString 里面。通過各種方式引導開發者少舊 checkType ,多用 checkTypOfString 。之后的項目迭代里面,合適的時候廢棄 checkType

5-2.函數違反單一原則

函數違反單一原則最大一個后果就是會導致邏輯混亂。如果一個函數承擔了太多的職責,不妨試下:函數單一原則 -- 一個函數只做一件事。

如下例子

  1. //現有一批的錄入學生信息,但是數據有重復,需要把數據進行去重。然后把為空的信息,改成保密。
  2. let students=[
  3. {
  4. id:1,
  5. name:'守候',
  6. sex:'男',
  7. age:'',
  8. },
  9. {
  10. id:2,
  11. name:'浪跡天涯',
  12. sex:'男',
  13. age:''
  14. },
  15. {
  16. id:1,
  17. name:'守候',
  18. sex:'',
  19. age:''
  20. },
  21. {
  22. id:3,
  23. name:'鴻雁',
  24. sex:'',
  25. age:'20'
  26. }
  27. ];
  28. function handle(arr) {
  29. //數組去重
  30. let _arr=[],_arrIds=[];
  31. for(let i=0;i<arr.length;i++){
  32. if(_arrIds.indexOf(arr[i].id)===-1){
  33. _arrIds.push(arr[i].id);
  34. _arr.push(arr[i]);
  35. }
  36. }
  37. //遍歷替換
  38. _arr.map(item=>{
  39. for(let key in item){
  40. if(item[key]===''){
  41. item[key]='保密';
  42. }
  43. }
  44. });
  45. return _arr;
  46. }
  47. console.log(handle(students))
復制代碼


運行結果沒有問題,但是大家想一下,如果以后,如果改了需求,比如,學生信息不會再有重復的記錄,要求把去重的函數去掉。這樣一來,就是整個函數都要改了。還影響到下面的操作流程。相當于了改了需求,整個方法全跪。城門失火殃及池魚。

下面使用單一原則構造一下

  1. let handle={
  2. removeRepeat(arr){
  3. //數組去重
  4. let _arr=[],_arrIds=[];
  5. for(let i=0;i<arr.length;i++){
  6. if(_arrIds.indexOf(arr[i].id)===-1){
  7. _arrIds.push(arr[i].id);
  8. _arr.push(arr[i]);
  9. }
  10. }
  11. return _arr;
  12. },
  13. setInfo(arr){
  14. arr.map(item=>{
  15. for(let key in item){
  16. if(item[key]===''){
  17. item[key]='保密';
  18. }
  19. }
  20. });
  21. return arr;
  22. }
  23. };
  24. students=handle.removeRepeat(students);
  25. students=handle.setInfo(students);
  26. console.log(students);
復制代碼

結果一樣,但是需求改下,比如不需要去重,把代碼注釋或者直接刪除就好。這樣相當于把函數的職責分離了,而且職責之前互不影響。中間去除那個步驟不會影響下一步。

  1. //students=handle.removeRepeat(students);
  2. students=handle.setInfo(students);
  3. console.log(students);
復制代碼
5-3.函數寫法優化

這種情況就是,對于以前的函數,在不影響使用的情況下,現在有著更好的實現方式。就使用更好的解決方案,替換以前的解決方案。

比如下面的需求,需求是群里一個朋友發出來的,后來引發的一些討論。給出一個20180408000000字符串,formatDate函數要處理并返回2018-04-08 00:00:00

以前的解法

  1. let _dete='20180408000000'
  2. function formatStr(str){
  3. return str.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, "$1-$2-$3 $4:$5:$6")
  4. }
  5. formatStr(_dete);
  6. //"2018-04-08 00:00:00"
復制代碼

后來研究了這樣的解法。這個方式就是根據x的位置進行替換填充數據,不難理解

  1. let _dete='20180408000000'
  2. function formatStr(str,type){
  3. let _type=type||"xxxx-xx-xx xx:xx:xx";
  4. for(let i = 0; i < str.length; i++){
  5. _type = _type.replace('x', str[i]);
  6. }
  7. return _type;
  8. }
  9. formatStr(_dete);
  10. result:"2018-04-08 00:00:00"
復制代碼

在之后的幾天,在掘金一篇文章(那些優雅靈性的JS代碼片段,感謝提供的寶貴方式)的評論里面發現更好的實現方式,下面根據上面的需求自己進行改造。

  1. let _dete='20180408000000'
  2. function formatStr(str,type){
  3. let i = 0,_type = type||"xxxx-xx-xx xx:xx:xx";
  4. return _type .replace(/x/g, () => str[i++])
  5. }
  6. formatStr(_dete);
  7. result:"2018-04-08 00:00:00"
復制代碼
5-4.代碼復用

上面幾個例子都是js的,說下與html沾邊一點的兩個例子--vue數據渲染。

下面代碼中,payChannelEn2Cn addZero formatDateTime函數都是在vue的methods里面。大家注意。

以前寫法

  1. <span v-if="cashType==='cash'">現金</span>
  2. <span v-else-if="cashType==='check'">支票</span>
  3. <span v-else-if="cashType==='draft'">匯票</span>
  4. <span v-else-if="cashType==='zfb'">支付寶</span>
  5. <span v-else-if="cashType==='wx_pay'">微信支付</span>
  6. <span v-else-if="cashType==='bank_trans'">銀行轉賬</span>
  7. <span v-else-if="cashType==='pre_pay'">預付款</span>
復制代碼

這樣寫的問題在于,首先是代碼多,第二是如果項目有10個地方這樣渲染數據,如果渲染的需求變了。比如銀行轉賬的值從 bank_trans 改成 bank ,那么就得在項目里面修改10次。時間成本太大。
后來就使用了下面的寫法,算是一個小重構吧

  1. <span>{{payChannelEn2Cn(cashType)}}</span>
復制代碼

payChannelEn2Cn 函數,輸出結果

  1. payChannelEn2Cn(tag){
  2. let _obj = {
  3. 'cash': '現金',
  4. 'check': '支票',
  5. 'draft': '匯票',
  6. 'zfb': '支付寶',
  7. 'wx_pay': '微信支付',
  8. 'bank_trans': '銀行轉賬',
  9. 'pre_pay': '預付款'
  10. };
  11. return _obj[tag];
  12. }
復制代碼

還有一個例子就是時間戳轉時間的寫法。原理一樣,只是代碼不同。下面是原來的代碼。

  1. <span>{{new Date(payTime).toLocaleDateString().replace(/\//g, '-')}}
  2. {{addZero(new Date(payTime).getHours())}}:
  3. {{addZero(new Date(payTime).getMinutes())}}:
  4. {{addZero(new Date(payTime).getSeconds())}}</span>
復制代碼

addZero時間補零函數

  1. Example:3->03
  2. addZero(i){
  3. if (i < 10) {
  4. i = "0" + i;
  5. }
  6. return i;
  7. }
復制代碼

問題也和上面的一樣,這里就不多說了,就寫重構后的代碼

  1. <span>{{formatDateTime(payTime)}} </span>
復制代碼

formatDateTime函數,格式化字符串

  1. formatDateTime(dateTime){
  2. return `${new Date(payTime).toLocaleDateString().replace(/\//g, '-')} ${this.addZero(new Date(payTime).getHours())}:${this.addZero(new Date(payTime).getMinutes())}:${this.addZero(new Date(payTime).getSeconds())}`;
  3. }
復制代碼

可能很多人看到這里,覺得重構很簡單,這樣想是對的,重構就是這么簡單。但是重構也難,因為重構一步登天,需要一個逐步的過程,甚至可以說重構就是一次次的小改動,逐步形成一個質變的過程。如何保證每一次的改動都是有意義的改善代碼;如何保證每一次的改動都不會影響到項目的正常使用;如果發現某次改動沒有意義,或者改動了反而讓代碼更糟糕的時候,可以隨時停止或回滾代碼,這些才是重構的難點。

6.小結

關于重構就說到這里了,該文章主要是介紹重構,例子方面都是很簡單的一些例子。目的是為了好些理解重構的一些概念。關于重構,可能很復雜,可能很簡單。怎么重構也是具體情況,具體分析,重構也沒有標準的答案。以后,如果有好的例子,我會第一時間分享,給大家具體情況,具體分析的講述:為什么重構,怎么重構。

最后,如果大家對文章有什么建議,看法,歡迎交流,相互學習,共同進步。



回復

使用道具 舉報