重构 - 改善代码的各方面问题

开发 前端
重构不是对以前代码的全盘否定,而是利用更好的方式,写出更好,更有维护性代码。不断的追求与学习,才有更多的进步。

[[227146]]

重构不是对以前代码的全盘否定,而是利用更好的方式,写出更好,更有维护性代码。不断的追求与学习,才有更多的进步。

1.前言

做前端开发有一段时间了,在这段时间里面,对于自己的要求,不仅仅是项目能完成,功能正常使用这一层面上。还尽力的研究怎么写出优雅的代码,性能更好,维护性更强的代码,通俗一点就是重构。这篇文章算是我一个小记录,在此分享一下。该文章主要针对介绍,例子也简单,深入复杂的例子等以后有适合的实例再进行写作分享。如果大家对怎么写出优雅的代码,可维护的代码,有自己的见解,或者有什么重构的实力,欢迎指点评论。

关于重构,准备写一个系列的文章,不定时更新,主要针对以下方案:逻辑混乱重构,分离职责重构,添加扩展性重构,简化使用重构,代码复用重构。其中会穿插以下原则:单一职责原则,最少知识原则,开放-封闭原则。如果大家对重构有什么好的想法,或者有什么好的实例,欢迎留言评论,留下宝贵的建议。

2.什么是重构

首先,重构不是重写。重构大概的意思是在不影响项目的功能使用前提下,使用一系列的重构方式,改变项目的内部结构。提高项目内部的可读性,可维护性。

无论是什么项目,都有一个从简单到复杂的一个迭代过程。在这个过程里面,在不影响项目的使用情况下,需要不断的对代码进行优化,保持或者增加代码的可读性,可维护性。这样一来,就可以避免在团队协作开发上需要大量的沟通,交流。才能加入项目的开发中。

3.为什么重构

衣服脏了就洗,破了就补,不合穿就扔。

随着业务需求的不断增加,变更,舍弃,项目的代码也难免会出现瑕疵,这就会影响代码的可读性,可维护性,甚至影响项目的性能。而重构的目的,就是为了解决这些瑕疵,保证代码质量和性能。但是前提是不能影响项目的使用。

至于重构的原因,自己总结了一下,大概有以下几点

  1.     函数逻辑结构混乱,或因为没注释原因,连原代码写作者都很难理清当中的逻辑。
  2.     函数无扩展性可言,遇到新的变化,不能灵活的处理。
  3.     因为对象强耦合或者业务逻辑的原因,导致业务逻辑的代码巨大,维护的时候排查困难。
  4.     重复代码太多,没有复用性。
  5.     随着技术的发展,代码可能也需要使用新特性进行修改。
  6.     随着学习的深入,对于以前的代码,是否有着更好的一个解决方案。
  7.     因为代码的写法,虽然功能正常使用,但是性能消耗较多,需要换方案进行优化

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.     }  

这个 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. //调用方式  
  45. //使用mobile校验规则  
  46. console.log(checkType.check('188170239','mobile'));  
  47. //添加金额校验规则  
  48. checkType.addRule('money',function (str) {  
  49.     return /^[0-9]+(.[0-9]{2})?$/.test(str)  
  50. });  
  51. //使用金额校验规则  
  52. 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.  
  43. console.log(checkType('188170239','mobile'));  
  44.  
  45. checkType('money',function (str) {  
  46.     return /^[0-9]+(.[0-9]{2})?$/.test(str)  
  47. });  
  48. //使用金额校验规则  
  49. 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.  
  29. function handle(arr) {  
  30.     //数组去重  
  31.     let _arr=[],_arrIds=[];  
  32.     for(let i=0;i<arr.length;i++){  
  33.         if(_arrIds.indexOf(arr[i].id)===-1){  
  34.             _arrIds.push(arr[i].id);  
  35.             _arr.push(arr[i]);  
  36.         }  
  37.     }  
  38.     //遍历替换  
  39.     _arr.map(item=>{  
  40.         for(let key in item){  
  41.             if(item[key]===''){  
  42.                 item[key]='保密' 
  43.             }  
  44.         }  
  45.     });  
  46.     return _arr;  
  47.  
  48. 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];  

还有一个例子就是时间戳转时间的写法。原理一样,只是代码不同。下面是原来的代码。

 

  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;  

问题也和上面的一样,这里就不多说了,就写重构后的代码

 

  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())}`;  

 

可能很多人看到这里,觉得重构很简单,这样想是对的,重构就是这么简单。但是重构也难,因为重构一步登天,需要一个逐步的过程,甚至可以说重构就是一次次的小改动,逐步形成一个质变的过程。如何保证每一次的改动都是有意义的改善代码;如何保证每一次的改动都不会影响到项目的正常使用;如果发现某次改动没有意义,或者改动了反而让代码更糟糕的时候,可以随时停止或回滚代码,这些才是重构的难点。

6.小结

 

关于重构就说到这里了,该文章主要是介绍重构,例子方面都是很简单的一些例子。目的是为了好些理解重构的一些概念。关于重构,可能很复杂,可能很简单。怎么重构也是具体情况,具体分析,重构也没有标准的答案。以后,如果有好的例子,我会***时间分享,给大家具体情况,具体分析的讲述:为什么重构,怎么重构。 

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

2010-07-09 14:46:56

2011-12-12 10:06:14

解析网站

2023-08-27 14:48:19

开源办公套件应用程序

2013-05-13 09:36:28

ClouderaImpala 1.0Hadoop

2009-10-16 10:44:17

2014-10-27 09:51:19

Web设计HTML

2021-08-03 08:13:48

重构API代码

2018-11-26 08:23:36

物联网客户体验IOT

2021-05-31 19:04:50

低代码平台低代码开发

2019-09-12 08:00:00

Visual Stud软件开发

2012-07-27 10:30:12

重构

2012-02-23 00:22:55

2010-10-20 09:25:49

网络成本

2019-02-18 16:21:47

华为代码重构

2011-08-16 09:47:58

编程

2010-04-02 12:24:08

2020-12-08 06:24:08

Java 代码工具

2019-04-03 08:10:17

代码架构信息

2022-12-26 00:02:24

重构代码软件

2011-07-06 09:40:24

云计算红帽
点赞
收藏

51CTO技术栈公众号