• 代码重构不是笑谈


    此文,基于《重构-改善既有代码的设计》第2版,的学习之后的一些想法.

    什么是重构?

    重构是在不改变软件可视范围内的对代码的调整,主要提高代码可读性,降低修改成本.
    在这本书中,任何一个重构方法的介绍,作者总在强调一件事情,重构代码是,请注意测试,稳定是重构的基本原则.

    什么时候重构?

    在代码逻辑不断增加的时候,也许有人想起重构代码,但是,每一次有这个念头,总会被无数个理由击败.运行的好好的,不要动他了,又不是不能用;重构会浪费很多的时间;排期紧;又不是我写的;代码太长了,太难理解,万一搞坏了怎么办;
    所以,一个很重要的问题?什么时候重构呢?
    这里我将其分为两类

    一:立刻重构

    需要立刻重构的代码,具备一个特性,那就是影响不大,且能随手解决.
    1.添加新功能时
    当你添加新功能时,发现又一些代码可以被提炼复用、或者同一模块有一些可以优化的地方,可以随手优化一下;
    2.修复bug时
    修复bug时,可以随手做一些优化,但不建议做太多的优化,毕竟解决bug时最紧急的事情;
    3.需要理解代码,梳理逻辑时
    这是一个很重要的时机,理解代码或数据逻辑时,当你发现代码的可读性很差,需要的成本很高时,这部分代码就已经到了需要重构的时候了;

    二:排期重构

    排期重构,就需要我们专门有一段时间去进行代码重构,可能是2天~2周.
    1.小型重构-有计划的重构
    代码到了需要专门需要拿出一段时间去重构的时候,问题就已经很严重了,但是,此时,建议,每一次专门的重构,时间在1~2周内,长时间段的重构,会让开发变得极端;
    同时,建议,专门的重构,可以根据模块进行重构,或者根据某一类问题进行重构
    比如,在同一此计划中,只针对于鉴权模块/组织架构模块重构;或者,此次重构,只重构if/else类型的代码;
    重构的集中,能够让重构的效率更高;
    2.大型重构-比如更新某个依赖库
    在一个较为庞大的项目中,修改依赖库,会触发比较多的改动,需要专门进行重构,并安排足够的测试;
    3.code review时
    这个一个非常好的时机,如果是团队每周代码review一次,那重构的时间也就一天或者半天,如果,一月一次review,可能就要有一个重构时间了;

    什么时候需要重构

    红色:我有自己的想法
    绿色:不需要解释的异味
    
    • 1
    • 2
    • 神秘命名
    • 重复代码
    • 过长函数
    • 过长参数列表
    • 全局数据
      • 变量放全局,各处使用随时赋值,永远不知道是哪个逻辑的bug
    • 可变数据
      • 在不同的地方,修改更新了同一个变量的数据,导致数据混乱!
      • 优化:

    1.封装变量
    2.封装变量为类,即以查询取代派生变量

    • 发散式变化
      • 例子:在一个类中,加入一个新的数据库,需要修改3个函数、修改这个引用,我需要改动5个函数

    这样,将数据关心的数据挪移到同一个类中进行统一管理输出.
    在调用方,只关心一个调用,不关心数据的来源

    • 霰弹式修改
      • 修改某个变化,就要在多个类中进行各种细微的修改,此时,就可以将这些相关的类挪移到同一个类中,进行管理
    • 依恋情结
      • 一个类中的的函数与其他函数或类的数据交换特别多,那么如此严重的数据依赖,为什么不考虑转移函数到其他的相关类中呢
    • 数据泥团
      • 同样的数据结构出现在多个地方,可以抽离出来作为类处理
    • 基本类型偏执
      • 过度使用基本类操作,如果有大量关于某一类的运算或者断言,可以进行封装结构或者多态方式替代
    • 重复的switch
      • 重复的switch可以被提炼,并不代表这所有的switch都必须使用多态来处理
    • 循环语句
      • 用管道符更优雅的处理循环,比如filter和map
      • 但是在某些情况下,原生的写法比管道符的效率更高,所以,管道符,视情况而定
        • 例子:

    for/forEach/map区别

    • 冗余的元素
      • 随着代码量的增加,一些函数或者数据会被增加很多冗余的元素,使用类或者内联函数,清晰提取相关数据元素,进行统一处理
    • 夸夸其谈的通用性
      • 缩小类的使用范围,做不必要的通用性缩减,抽离的超类,没有那么大的责任,就可以放到小的模块中
    • 临时字段
      • 创建的临时字段,有时候,你的意思就是临时作用,然而,你不知道它的临时结束日期是什么时候,所以,可以使用搬移函数将相关函数或字段放入新的类中,或者引入特例,创建你一个替代对象,避免写条件式代码
    • 过长的消息链
      • 不好解释,直接上代码
    let notice = require("./notice");
    let people = notice.info.sendMessage.list;
    
    • 1
    • 2
    • 中间人
      • 过度的隐藏内部结构
    • 内幕交易
      • 类内的数据交换异常频繁,可以考虑将数据单独作为类处理
    • 过大的类
      • 一个类中拥有过多的函数,承担过重的逻辑
    • 异曲同工的类
      • 拥有差不多功能的类,可以被合并
    • 纯数据类
      • 在纯数据类中创建查看/修改函数
    • 被拒绝的遗赠
      • 当子类不需要使用超类的函数或数据,可以使用委托代替子类
    • 注释
      • 注释是好的,但是,当你的代码需要过多的注释才能让人看懂的话,是不是可以考虑语义化命名函数、拆分函数等等操作

    重构的方法

    重构的方法,书中讲解了很多,这里,会解释大部分重构方法,当然,一些显而易见的就不再赘述了;

    1.简单的重构方法

    字段重命名、函数重命名、提炼函数、内联函数、封装函数

    2.封装变量

    在重构过程中,尤其是重构函数时,入参的排列顺序及注释会产生很大的误解,所以为了重构的方便性,以及管理入参,可以考虑使用封装变量的方法
    或者,当可变数据超出当前作用域的时候,就可以将其封装

    //旧代码
    function test(date){
      let {name,age,sex,remark} = date;
      ///...业务逻辑
      return getUser(name,age,sex);
    }
    
    //封装变量
    function test(date){
      let {name,age,sex,remark} = date;
      ///...业务逻辑
      let filter = {name,age,sex};
      return getUser(filter);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.拆分函数

    拆分函数,顾名思义,庞大的逻辑,不要想了,拆吧!!!想想一个函数500行,可怕啊😨
    可以细分功能,拆成不同的函数

    4.函数组合成类

    函数组合成类:首先,将共同类的函数组合到同一个类中,便于后期使用调用,相关模块内容;其次可以以少量的初始化数据,进行初始化类,同时,类内函数互相调用,可以较少入参的传输;

    5.封装数据

    类似于TS中的数据类型声明;
    通俗说法就是,将变量对象或需要处理的数据,或不想改变原有值的对象数据,将其封装在一个类中,暴露各种获取方法,产生逻辑所需要的各种数据!
    多用于数据转化、深拷贝、格式化数据等等…
    同时缺点也明显,在复制巨大数据结构时,性能消耗大,按需使用!

    //旧代码 user.js
    async function userInfo(userId){
      let ageList = [
        {age:1,money:1000,remark:"1"},
        {age:2,money:2000,remark:"2"},
        {age:3,money:2000,remark:"3"}
      ];
      let user = await User.getInfo(userId);
      return  ageList.find(item=>{
        if(item.age === user.age){
          item.sex = user.sex;
          item.name = user.name;
          return item;
        }
      });
    }
    
    
    //重构后
    //class类
    class User {
      constructor(ageInfo, user) {
        let {age, money, remark} = ageInfo;
        let {sex, name} = user;
        this.age = age;
        this.money = money;
        this.remark = remark;
        if (age === user.age) {
          this.sex = sex;
          this.name = name;
        }
      }
    }
    
    module.exports = User;
    
    
    //user.js
    let User = require("./user.class");
    async function userInfo(userId) {
      let ageList = [
        {age: 1, money: 1000, remark: "1"},
        {age: 2, money: 2000, remark: "2"},
        {age: 3, money: 2000, remark: "3"}
      ];
      let user = await User.getInfo(userId);
      if (item.age === user.age) {
        return new User(item,user);
      }
    });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    6.提炼类

    顾名思义,为了不让一个文件承担很多责任,可以提炼一些类或函数.如果觉得某些方法是很多地方都用的到,就提炼一个超类.

    7.隐藏委托关系/移除中间人

    简而言之,就是常见一个类,将客户端的链式调用,放到一个函数中,可以端只需要带哦用一个类就可以获取到想要的值,而不用关心数据之间的链式调用
    例如:客户端需要知道某人的经理是谁
    后端:
    Class Person

    constrctor(name){
      this._name = name
    }
    get name(){return this.name;}
    get department(){return this._department;}
    set department(arg){this._department = args;}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Class Deparment

    
    get manager(){return this._manager;}
    set manager(arg){this._manager = args;}
    
    • 1
    • 2
    • 3

    客户端调用:

    manager = aPerson.department.manager;
    
    • 1

    优化后:
    Class Person

    get manager(){return this._department.manager;}
    
    • 1

    客户端调用

    manager = aPerson.manager;
    
    • 1

    移除中间人:这种操作是按代码情况而定,当不需要隐藏委托关系时,就可以去除中间人。
    为什么要移除中间人,当隐藏的委托关系过多时,就完全变成了一个数据中转站,这并不是我们想要的操作,我们要的是轻量级,这样沉重的操作就需要被优化调!

    8.替换算法

    将一些复杂的算法,拆分为多了小的算法,最后组合长想要的数据,算法也是需要根据时代去变化的,比如,es6中的includs方法就很好的代替了es5中的find方法.

    9.以对象代替基本类型

    在重构一中,也称为以类取代类型码
    当一份数据不局限于展示时,就可以为其创建一个类,尽管初期很繁琐,的却,但是,当业务越来越复杂,这个类的好处就越明显!
    给这个数据一个取值函数,这是基础的!

    10. 搬移特性

    为什么说搬移特性,应为这里包很很多搬移,比如:搬移字段、搬移语句、搬移函数等等.
    有些人,在开发过程中可能想到什么就写什么,那么我们就可以在逻辑完成之后,对语句进行调整;
    主要的宗旨就在于,将相关的代码放到一起,便于理解和处理.如果搬移之后,你发现还可以提炼函数、提炼类,这都是可以优化的;

    11.拆分循环

    很简单的理解,尽量拆分循环中的操作,让一个循环只做一件事(数据计算),或者少数的事情,这样会便于后期修改和理解,因为,你知道每一个循环是为了做什么,而不是一大堆计算换入同一个循环中,你该懂的知识其中一个计算,却要理解所有计算,你的操作会不会影响到其他操作!
    重构提醒:不要担心拆分循环会造成效率低下,我们有更多的方法可以提升效率.

    //旧代码
    function loop() {
      let array = [
        {key: 1, sex: 1},
        {key: 2, sex: 1},
        {key: 3, sex: 1},
        {key: 4, sex: 1},
      ];
      let num = 0;
      for (let i = 0; i < array.length; i++) {
        num += array[i].key;
        if (array[i].sex === 1) {
          array[i].sexName = "男";
        }
      }
    }
    
    //重构后
    function loop() {
      let array = [
        {key: 1, sex: 1},
        {key: 2, sex: 1},
        {key: 3, sex: 1},
        {key: 4, sex: 1},
      ];
      let num = 0;
      for (let i = 0; i < array.length; i++) {
        num += array[i].key;
      }
      for (let j = 0; j < array.length; j++) {
        if (array[j].sex === 1) {
          array[j].sexName = "男";
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    12.以管道代替循环

    在操作系统和代码进步的时代,各种管道符可以让代码的可读性更强.
    示例:
    在函数中筛选出所有的印度的办公室,并返回办公室所在城市信息和联系电话
    原代码

    function acquireData(input) {
      const lines = input.splice("\n");
      let firstLine = true;
      const result = [];
      for (const line of lines) {
        if (firstLine) {
          firstLine = false;
          continue;
        }
        if (line.trim() === "") continue;
        const record = line.splice(",");
        if (record[1].trim() === "India") {
          result.push({city: record[0].trim(), phone: record[2].trim()})
        }
      }
      return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    管道符优化后

    function acquireData(input) {
      const lines = input.splice("\n");
      return lines.slice(1)
        .filter(line => line.trim() !== "")
        .map(line => line.splice(","))
        .filter(fields => fields[1].trim() === "India")
        .map(fields => ({city: fields[0].trim(), phone: fields[2].trim()}));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    13.分解条件表达式

    比较沉重的业务功能,可能拥有较为复杂的条件逻辑,在大型功能中,冗长的函数让人头大,且毫无修改心思,谁知道会不会触发罗七八糟的逻辑,而且,为了一个小修改,就要通读冗杂的逻辑,而你其实,只需要其中一个小改动.
    这是提取函数的一个具体分之,将一段的冗长的代码分解为多个小函数,配合语义化函数,这样,修改的时候,你只需要去查看那几个逻辑即可.

    14.合并条件表达式

    这是和13对立的方法,但是,不可否认,这是一个好的方法,有些人在条件逻辑中,条件不同,但输出完全一致,而事实上,我们可以把他们合并起来,让代码更优雅

    示例:

    //源逻辑
    let test = {
      "name": "lock",
      age: null
    }
    if (test.name) {
      if (test.age) {
        console.log("test", test);
        return true;
      }
    }
    return false;
    
    
    
    
    
    //优化后,合并条件表达式
    let test = {
      "name": "lock",
      age: null
    }
    if (test.name && test.age) {
      console.log("test", test);
      return true;
    }
        return false;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    15.以卫语句取代嵌套条件表达式

    这个方法的精髓就在于,在if/else语句中,给某一分之以特别重视,即,当出现此条件是,立刻返回/退出当前函数.

    //旧代码
    function ifCode() {
      let age = 1;
      let result = false;
      if (age === 1) {
        result = true;
      } else {
        result = false;
      }
      return result;
    }
    
    //重构代码
    function ifCode() {
      let age = 1;
      let result = false;
      if (age === 1) {
        result = true;
        return result;
      }
      return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    16.以多态取代条件表达式

    在复杂逻辑处理时,我们经常会在一个条件内,处理很长的函数,同时,此方法,拥有多个不同的条件,且处理方案均不相同,此时,多态便尤为重要!

    这是一个简单的例子

    //源逻辑
    function caseInfo(name){
      let discountCaseId;
      if(name === "one"){
        discountCaseId = 1;
      }else if(name === "two") {
        discountCaseId = 7;
      }
      return discountCaseId;
    }
    
    //重构后代码逻辑
    function caseMap(name){
      const caseMap = {one: 1, two: 7};
      const discountCaseId = caseMap[name];
      return discountCaseId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当然,用于处理条件逻辑是这样的

    //源逻辑
    async revenueDaily(data) {
      let {store_id, tenancy_id, report_date, upload_type, list} = data;
      let token = await Order.getToken();
      if(upload_type === "2"){
        //...
        return NCC.post(ROUTE.SALE_ORDER, saleData, token);
      }else if(upload_type === "3"){
        //...
        return NCC.post(ROUTE.RECEIVABLE, receivableData, token);
      }else if(upload_type === "4"){
        //...
        return Order.receivableForAllStores(data, token);
      }
    }
    
    
    //重构后的逻辑
    async revenueDaily(data) {
      let {store_id, tenancy_id, report_date, upload_type, list} = data;
      let token = await Order.getToken();
      return uploadFunction[upload_type](token, {report_date, store_id, tenancy_id, list});
    }
    
    let uploadFunction = {
      "2": async (token, data) => {
        //...
        return NCC.post(ROUTE.SALE_ORDER, saleData, token);
      },
      "3": async (token, data) => {
        //...
        return NCC.post(ROUTE.RECEIVABLE, receivableData, token);
      },
      "4": async (token, data) => {
        //...
        return Order.receivableForAllStores(data, token);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    17.引入特值

    这种方法适用于,代码中某个特殊的值或情况经常出现,切处理逻辑相同,即可采用引入特值方法,本质上,他和萃取函数相同,不过可以理解为对极限情况或固定情况的处理;
    常见的特值处理有,判断字符是否为不存在、判断邮箱格式是否正确.

    18.将查询函数和修改函数分离

    任何有返回值的函数,都不应该看得到副作用!

    将无论怎么查询,返回值都是一样的结果存储在某个缓存中,这样可以为后续的查询大大减少时间;
    如果是一个既有返回值又有副作用的函数,可以试着将查询从修改中分离出来,变成可复用的查询;

    19.移除标记类型

    一些标记类型的函数取值,让人不得不去扒拉代码,尤其是,你的函数参数中有一个布尔值时,你讲不知道这个true/false是想执行什么逻辑,那么此时,我们就可以将其需要执行的函数拆分为两个函数,并在函数调用时,将拆分函数作为参数写入源函数中,这样,你就明确的知道,这个值时做什么的!

    20.以查询取代参数

    此优化方案,有利有弊。
    利为:简化了调用方的操作,调用方用了更少的操作、更少的参数完成了功能;
    弊为:可能会给函数造成不必要的依赖关系,增加开发者维护成本,此弊端,其实可以用小颗粒度的功能函数来弥补,同时查询函数的结果要具备确定性,即:无论什么时候,我传入固定的值,返回值是相同的。

    21.以工厂函数取代构造函数

    简而言之,此方法就是,创建一个工厂函数,用于操作new类动作,用来简化逻辑理解。

    22.移动代码

    包括:函数上移、字段上移、构造函数本体上移至超类
    以及反向操作
    函数下移、字段下移、构造函数本体上移至子类
    相关介绍可以查看另一篇详细说明
    《重构2》第十二章-继承

    23.以委托取代子类/以委托取代超类

    以委托取代子类:
    在需要出处理多个平行逻辑判断的时候,为了清楚的展示逻辑,可以创建一个中间类,在中间类里,进行工厂函数构建,来处理多个平行处理逻辑,同时,如果有些函数如果比较基础,也可以挪移至超类中,进行swich/case操作;在调用方使用时,只需要使用中间类就可以判断各种逻辑执行的出口,此时中间类相当于一个水龙头的不同的出水口;
    以委托取代超类:
    主要用于优化子类继承超类的list,为了避免子类的修改操作影响到超类的所有数据,因此,将部分属性作为派生子类创建,然后在派生子类中修改子类的数据,而在超类中,如果想获取/修改当前属性,只需要将派生子类在超类中获取为一个属性,通过属性转发各派生子类属性的获取/修改即可.

    🤏一点想法

    重构,不是无法进行,也不是可以不进行.所谓重构,无外乎一些好的代码习惯,加上一些功能模块完成是的代码优化.好的代码习惯,其实已经在开发中就在执行优化重构了.
    在重构时,我们提取的优秀的功能点,函数,都是让人眼前一亮,心旷神怡的.既然优秀,为什么不封装起来呢?
    小步快走,重构并不难!但是别步子太大,容易摔!

    ⚠️些许告诫

    1.重构,一定要进行代码检测/单元测试,越庞大的项目,越需要严谨的单元测试,这样,才不至于,你的重构造成项目雪崩!
    2.重构,不是一撮而就,小步快走,谨慎前行.
    3.杜绝拷贝,在开发中,我们已经有了代码自动补全,如果你在开发一个功能,需要重复的逻辑,是不是可以考虑提炼函数?如果你是借鉴其他项目的操作,自己按着别人的逻辑敲一次,理解别人的思路,也是让自己对这个功能有更深的理解.

    补充

    重构的方法有很多,这里只说了一些我觉得重要且常用的,如果想看全部的重构方法,可以查看我的读书笔记
    代码坏味道

  • 相关阅读:
    论文精读Transformer: Attention is all you need
    图论|841钥匙和房间
    【工作笔记】数据库死锁的排查和数据库锁知识记录
    牛客:FZ12 牛牛的顺时针遍历
    leet hot 100-7 无重复字符的最长子串
    2022-2028全球视频监控软件行业调研及趋势分析报告
    数据库基础---SQL语句(基于sql server的笔记)
    【Linux升级之路】6_进程间通信
    路由器选择使用指南这三Openwrt
    马尔科夫链(Markov Chain, MC)算法详解及Python实现
  • 原文地址:https://blog.csdn.net/Long861774/article/details/125617888