名词解释
重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的关键在于 运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。
什么是重构 ?
重构不是重写。
重构大概的意思是在不影响项目的功能使用前提下,使用一系列的重构方式,改变项目的内部结构。提高项目内部的可读性,可维护性。
无论是什么项目,都有一个从简单到复杂的一个迭代过程。
在这个过程里面,在不影响项目的使用情况下,需要不断的对代码进行优化,保持或者增加代码的可读性,可维护性。这样一来,就可以避免在团队协作开发上需要大量的沟通,交流。
重构的时机
合适时机
不应该等到写不下去的时候 或者 有了瓶颈之后再进行重构,大规模高层次的重构耗时耗力难度剧大
应该建立起渐进式持续重构的意识,发现当前业务代码写的有问题就应该及时进行小规模的重构,而不是欠一屁股技术债
重构应该是在开发过程中实时的、渐进的演化过程。
不恰当时机
不是所有软件开发过程都一定要重构
有一些场景的重构价值就很小:
- 代码库(项目)生命周期快要走到尾声,开发逐渐减少,以维护为主。
- 代码库(项目)当前版本马上要发布了,这时重构无疑是给自己找麻烦。
- 重构代价过于沉重:重构后功能的正确性、稳定性难以保障;技术过于超前,团队成员技术迁移难度太大。
SOLID原则
什么是 SOLID 原则 ?
| 缩写 | 英文名称 | 中文名称 |
|---|---|---|
| SRP | Single Responsibility Principle | 单一职责原则 |
| OCP | Open Close Principle | 开闭原则 |
| LSP | Liskov Substitution Principle | 里氏替换原则 |
| ISP | Interface Segregation Principle | 接口分离原则 |
| DIP | Dependency Inversion Principle | 依赖倒置原则 |
Single Responsibility Principle(SRP)单一职责原则
一个类或者一个模块只做一件事。
Interface Segregation Principle(ISP)接口隔离原则
一个接口应该拥有尽可能少的行为,使其精简单一。
对于不同的功能的模块分别使用不同接口,而不是使用同一个通用的接口。
Open Closed Principle(OCP)开放封闭原则
对扩展开放,对修改关闭。
体现在实践当中,就是类和模块之间的依赖,应该基于接口,而不是具体的实现。应该基于父类,而不是子类。
前端有一个实践,有点类似这种思想: VUE 的Slot
Liskov Substitution Principle(LSP)里氏替换原则
所有基类出现的地方都可以用派生类替换而不会让程序产生错误,派生类可以扩展基类的功能,但不能改变基类原有的功能。
Dependence Inversion Principle(DIP)依赖注入原则
高级模块不应该依赖低级模块,应该依赖抽象。细节应该依赖抽象,抽象不可以依赖细节。
为什么要重构 ?
改进软件的设计
如果没有重构,程序的内部设计会随着时间的增加逐渐腐败变质。
很多开发人员在改bug修改代码时,经常没有完全理解程序的架构设计,就临时修补程序,于是今天张三改一下明天李四改一下,于是代码逐渐失去了自己的结构。
会导致代码越愈发腐败得更快,最终沦为一个谁也不敢动的项目。
提高编程速度
当代码 腐败到一定程度之后 想要添加一个新功能时,需要的时间会比之前的时间要长很多
开发人员需要花更多的时间去思考,去考虑如何把新功能塞进现有的代码库中,避免因改一个地方而出现牵一发而动全身的尴尬境界
整个项目的代码库看起来就像是在补丁上再补上补丁
大多数需要重构的原因
- 函数逻辑结构混乱,或因为没注释原因,连原代码写作者都很难理清当中的逻辑。
- 函数无扩展性可言,遇到新的变化,不能灵活的处理。
- 因为对象强耦合或者业务逻辑的原因,导致业务逻辑的代码巨大,维护的时候排查困难。
- 重复代码太多,没有复用性。
- 随着技术的发展,代码可能也需要使用新特性进行修改。
- 随着学习的深入,对于以前的代码,是否有着更好的一个解决方案。
- 因为代码的写法,虽然功能正常使用,但是性能消耗较多,需要换方案进行优化
何时需要重构 ?
重构可以说是贯穿整一个项目的开发和维护周期,可以当作重构就是开发的一部分。
通俗讲,在开发的任何时候,只要看到代码有别扭,激发了强迫症,就可以考虑重构了。
只是,重构之前先参考下面几点。
-
- 首先,重构是需要花时间去做的一件事。花的时间可能比之前的开发时间还要多。
- 其次,重构是为了把代码优化,前提是不能影响项目的使用。
- 最后,重构的难度大小不一,可能只是稍微改动,可能难度比之前开发还要难。
基于上面的几点,需要大家去评估是否要进行重构。评估的指标,可以参考下面几点
-
- 数量: 需要重构的代码是否过多。
- 质量: 可读性,可维护性,代码逻辑复杂度,等问题,对代码的质量影响是否到了一个难以忍受的地步。
- 时间: 是否有充裕的时间进行重构和测试。
- 效果: 如果重构了代码,得到哪些改善,比如代码质量提高了,性能提升了,更好的支持后续功能等。。
重构时常见的 "坏味道的代码"
函数违反单一原则
函数违反单一原则最大一个后果就是会导致逻辑混乱。
//现有一批的录入学生信息,但是数据有重复,需要把数据进行去重。然后把为空的信息,改成保密。
let students=[
{
id:1,
name:'守候',
sex:'男',
age:'',
},
{
id:2,
name:'浪迹天涯',
sex:'男',
age:''
},
{
id:1,
name:'守候',
sex:'',
age:''
},
{
id:3,
name:'鸿雁',
sex:'',
age:'20'
}
];
function handle(arr) {
//数组去重
let _arr=[],_arrIds=[];
for(let i=0;i<arr.length;i++){
if(_arrIds.indexOf(arr[i].id)===-1){
_arrIds.push(arr[i].id);
_arr.push(arr[i]);
}
}
//遍历替换
_arr.map(item=>{
for(let key in item){
if(item[key]===''){
item[key]='保密';
}
}
});
return _arr;
}
console.log(handle(students))
运行结果没有问题,但是大家想一下,如果以后,如果改了需求,
比如,学生信息不会再有重复的记录,要求把去重的函数去掉。
这样一来,就是整个函数都要改了。还影响到下面的操作流程。相当于了改了需求,整个方法全跪。
let handle={
removeRepeat(arr){
//数组去重
let _arr=[],_arrIds=[];
for(let i=0;i<arr.length;i++){
if(_arrIds.indexOf(arr[i].id)===-1){
_arrIds.push(arr[i].id);
_arr.push(arr[i]);
}
}
return _arr;
},
setInfo(arr){
arr.map(item=>{
for(let key in item){
if(item[key]===''){
item[key]='保密';
}
}
});
return arr;
}
};
students=handle.removeRepeat(students);
students=handle.setInfo(students);
console.log(students);
神秘的命名
一个好的命名,能让读者一眼就清楚代码的意思,整洁代码中最重要的一环是从好的命名开始
神仙数字
代码中存在神仙数字 不看实现函数是不知道 type 1 2 3 4 是代表什么 1,2,3,4可以说是一个神仙数
/**
* @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);
为了解决这个问题,处理方式就可以分拆 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, "");
}
重复代码
在优秀的开发者心中,践行着事不过三的原则,即一段代码在三处以上的地方用到时,便是开始重构的时候
像下方的 重复代码 就可以采用策略模式来进行重构
<span v-if="cashType==='cash'">现金</span>
<span v-else-if="cashType==='check'">支票</span>
<span v-else-if="cashType==='draft'">汇票</span>
<span v-else-if="cashType==='zfb'">支付宝</span>
<span v-else-if="cashType==='wx_pay'">微信支付</span>
<span v-else-if="cashType==='bank_trans'">银行转账</span>
<span v-else-if="cashType==='pre_pay'">预付款</span>
<span>{{payChannelEn2Cn(cashType)}}</span>
payChannelEn2Cn(tag){
let _obj = {
'cash': '现金',
'check': '支票',
'draft': '汇票',
'zfb': '支付宝',
'wx_pay': '微信支付',
'bank_trans': '银行转账',
'pre_pay': '预付款'
};
return _obj[tag];
}
过长函数
有大佬曾讲过,但凡是一个函数如果超过50行以上的代码,就应该开始考虑进行重构它。
过长参数列表
- 比如一个函数 需要 4 - 5 个参数 那么就需要考虑 该函数是否还需要进一步的拆分
- 过长的参数列表会经常令人产生迷惑,容易让人犯错,我们可以把多个参数合并成一个对象,通过传递对象的方式减少过长的参数列表,从而让代码更简洁已读。
/**
* 获取票据信息
*
* @param name 姓名
* @param age 年龄
* @param isChild 是否儿童
* @param isStudent 是否学生
* @param ageFloor 年龄上限
* @param ageCeiling 年龄下限
* @param performance 演出信息
* @param basicPrice 基本票价
* @return 票据信息
*/
function getTicketInfo( name, age, isChild, isStudent, ageFloor, ageCeiling,
performance, basicPrice) {
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
getTicketInfo(name, age, isChild, isStudent, ageFloor, ageCeiling, performance, basicPrice)
function getTicketInfo( { name, age, isChild, isStudent, ageFloor, ageCeiling,
performance, basicPrice} = {} ) {
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
getTicketInfo( { name:'张三', age: 18 } )
"死"代码
一段永远不会执行的代码 一般是需求或者场景变更频繁 漏删除的代码
或者 一些不必要的引用的文件
过于臃肿的判断条件
原代码
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
新代码
if (isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability() {
return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime));
}