重构

320 阅读9分钟

名词解释

重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构的关键在于 运用大量微小且保持软件行为的步骤,一步步达成大规模的修改

什么是重构 ?

重构不是重写。

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

无论是什么项目,都有一个从简单到复杂的一个迭代过程。

在这个过程里面,在不影响项目的使用情况下,需要不断的对代码进行优化,保持或者增加代码的可读性,可维护性。这样一来,就可以避免在团队协作开发上需要大量的沟通,交流。

重构的时机

合适时机

不应该等到写不下去的时候 或者 有了瓶颈之后再进行重构,大规模高层次的重构耗时耗力难度剧大

应该建立起渐进式持续重构的意识,发现当前业务代码写的有问题就应该及时进行小规模的重构,而不是欠一屁股技术债

重构应该是在开发过程中实时的、渐进的演化过程。

不恰当时机

不是所有软件开发过程都一定要重构

有一些场景的重构价值就很小:

  • 代码库(项目)生命周期快要走到尾声,开发逐渐减少,以维护为主。
  • 代码库(项目)当前版本马上要发布了,这时重构无疑是给自己找麻烦。
  • 重构代价过于沉重:重构后功能的正确性、稳定性难以保障;技术过于超前,团队成员技术迁移难度太大。

SOLID原则

什么是 SOLID 原则 ?

缩写英文名称中文名称
SRPSingle Responsibility Principle单一职责原则
OCPOpen Close Principle开闭原则
LSPLiskov Substitution Principle里氏替换原则
ISPInterface Segregation Principle接口分离原则
DIPDependency 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修改代码时,经常没有完全理解程序的架构设计,就临时修补程序,于是今天张三改一下明天李四改一下,于是代码逐渐失去了自己的结构。

会导致代码越愈发腐败得更快,最终沦为一个谁也不敢动的项目。

提高编程速度

当代码 腐败到一定程度之后 想要添加一个新功能时,需要的时间会比之前的时间要长很多

开发人员需要花更多的时间去思考,去考虑如何把新功能塞进现有的代码库中,避免因改一个地方而出现牵一发而动全身的尴尬境界

整个项目的代码库看起来就像是在补丁上再补上补丁

大多数需要重构的原因

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

何时需要重构 ?

重构可以说是贯穿整一个项目的开发和维护周期,可以当作重构就是开发的一部分。

通俗讲,在开发的任何时候,只要看到代码有别扭,激发了强迫症,就可以考虑重构了。

只是,重构之前先参考下面几点。

    • 首先,重构是需要花时间去做的一件事。花的时间可能比之前的开发时间还要多。
    • 其次,重构是为了把代码优化,前提是不能影响项目的使用。
    • 最后,重构的难度大小不一,可能只是稍微改动,可能难度比之前开发还要难。

基于上面的几点,需要大家去评估是否要进行重构。评估的指标,可以参考下面几点

    • 数量: 需要重构的代码是否过多。
    • 质量: 可读性,可维护性,代码逻辑复杂度,等问题,对代码的质量影响是否到了一个难以忍受的地步。
    • 时间: 是否有充裕的时间进行重构和测试。
    • 效果: 如果重构了代码,得到哪些改善,比如代码质量提高了,性能提升了,更好的支持后续功能等。。

重构时常见的 "坏味道的代码"

函数违反单一原则

函数违反单一原则最大一个后果就是会导致逻辑混乱。

//现有一批的录入学生信息,但是数据有重复,需要把数据进行去重。然后把为空的信息,改成保密。
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行以上的代码,就应该开始考虑进行重构它。

过长参数列表

  1. 比如一个函数 需要 4 - 5 个参数 那么就需要考虑 该函数是否还需要进一步的拆分
  2. 过长的参数列表会经常令人产生迷惑,容易让人犯错,我们可以把多个参数合并成一个对象,通过传递对象的方式减少过长的参数列表,从而让代码更简洁已读。
 /**
     * 获取票据信息
     * 
     * @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));
}