浅读重构改善既有代码设计(第二版)

198 阅读9分钟

阅读背景

手头的项目是个五年的项目,经历了几代人的维护难免会出现很多问题,其中命名不规范、大文件、过长函数、重复代码可能是最常见的一些问题,为了比较规范的去解决这些问题,我浅读了一下《重构改善既有代码设计》这本书,找到了一些不错的方法,如果你的项目也有相同的问题,那么可以一起来看一下,希望能够给你带来帮助。我阅读的是第二版,第一版是用java写的,第二版是用js写的,所以对于前端工程师非常友好,大家可以放心食用。

坏味道之神秘命名

变量改名

这种要修改的变量只作用于某个函数的可以直接替换即可 image.png 给常量改名,替换过程中以新名字作为过渡,等全部替换完成之后才会删除旧的名字 image.png

封装变量

如果变量被广泛使用或者是修改,作用域超出的单个函数,就可以考虑运用封装变量的方法,用函数的形式封装所有对该数据的访问和修改 image.png 前面是控制了对于变量引用的修改,那么如果控制对于变量内容的修改呢?这里介绍两个方法:

第一种是禁止对数据结构内部的数值做任何修改,可以对于取值函数defaultOwner进行修改,使其返回该数据的一个副本 image.png 第二种是通过类封装去阻止对于数据的修改,尝试对其属性(即lastNamefirstName)重新赋值不会产生任何效果 image.png

关于 封装变量 我在代码中是如何应用的

原来的代码

//声明了一个groups的对象
let groups = {
    info: {
      whitelistType: "phone",
      partationType: "exp_bucket",
      paramsKey: []
    },
    buckets: [[0, 100]],
    datas: [
      {
        name: "control_group",
        description: "",
        whitelistValue: "",
        paramsVal: [],
        objects: []
      }
    ]
  };
  //满足某种条件的时候会对这个groups对象进行重新赋值(引用的修改)
  if(...){
      groups = {
          info: {
            whitelistType: "phone",
            partationType: "exp_bucket",
            paramsKey: []
          },
          buckets: [],
          datas: []
       };
  }
  //满足某种条件的时候会对groups对象深层参数内容进行修改
  if(...){
      groups.info.whitelistType = '123';
  }

改造后的代码

//封装变量 ——> 封装记录
//可变数据更倾向于类,不变的存储成对象
class Groups {
    constructor(data) {
        //存储修改的值
        this._data = data;
        //存储没有修改的值
        this.data = cloneDeep(data);//cloneDeep是lodash中的
    }
    //获取没有修改的值
    getInitialdata = () => this.data;
    //获取修改的值
    getData = () => this._data;

    //赋值
    setData = (aObj) => { 
        this._data = aObj;
        return this._data;
    };

    //提供清晰的API列表,清楚描述该类的全部用途
    setWhitelistType = (aStr) => {
        this._data.info.whitelistType = aStr
        return this._data;
    }
}
//实例化数据
const groups = new Groups({
    info: {
        whitelistType: "phone",
        partationType: "exp_bucket",
        paramsKey: []
    },
    buckets: [[0, 100]],
    datas: [
        {
            name: "control_group",
            description: "",
            whitelistValue: "",
            paramsVal: [],
            objects: []
        }
    ]
});
//获取数据  
getGroups = () => {
    return groups.getData();
}
//重新对数据赋值
setGroups = res => {
    return groups.setData(res);
}
//获取没有被修改的原始数据
getInitialdata = () => {
    return groups.getInitialdata();
}
//赋值
const changedGroups = setGroups({
    info: {
        whitelistType: "phone1",
        partationType: "exp_bucket",
        paramsKey: []
    },
    buckets: [],
    datas: []
});
console.log(JSON.stringify(changedGroups), '1被重新赋值的数据')
//更新读取更深层次的数据
const changedWhitelistType = groups.setWhitelistType(9999999999);
console.log(JSON.stringify(changedWhitelistType), '2被修改了深层次的数据')
//获取原始没有改变的值
const initValue = getInitialdata();
console.log(JSON.stringify(initValue), '3没有被修改的原始数据')

改变函数声明

这个改变函数声明,为什么不叫函数改名?其实它包含了两方面,但是做法都是一样的,第一就是给一个函数改一个好名字,先写一句注释描述这个函数的用途,再把这句注释变成函数的名字;第二就是参数,参数阐述了这个函数如何与外界函数共处,是传入一个大的对象作为参数?还是传递大对象中的某个值作为参数?这种没有对和错,随着时间变化的,所以掌握改变函数的手法很重要,这样函数才会随着我得理解而演进。

  • 简单做法:
    适用于确定了函数或者参数只在有限的小范围内使用,找到所有的函数/参数声明的地方将其改名,缺点是必须一次性修改完所有的调用者和函数声明 image.png
  • 迁移式做法(修改函数名)
    将函数体提炼成一个新函数,对旧函数使用内联函数来调用或返回新函数。其实这种思想非常适合去拆分一个大的文件,比如我得项目中有一个utils.js的文件,大家都知道这个文件里面有很多纯函数的处理方法,这个文件有2000多行需要进行拆分处理,我发现这个文件中有三个函数都超过了300行,其实将这三个函数利用迁移式做法进行拆分到对应的小文件中就可以将utils.js文件进行很大力度的瘦身,在小文件中定义对应的新函数,然后需要在utils.js文件中引入小文件中对应新函数,在老函数中进行调用,如下图所示: image.png

坏味道之重复代码

提炼函数

  • 简单做法: 当我们发现一大段函数内某一部分代码在做的事情是同一件事,并且自成体系,不与其他掺杂时就可以进行提炼。提炼函数可以解决重复代码的问题,但是它不仅仅解决重复代码的问题,就比如,当你需要花时间浏览一段代码才能弄清楚它到底在干什么,这时也可以将其提炼成一个函数,并根据他所做的事为其命名。以后再读这段代码时,你一眼就能看到函数的用途,大多数时候可能根本不需要关心函数如何实现的。 image.png
  • 局部变量提取: 被提取的局部变量被修改了,需要将逻辑也一起提炼出去

image.png image.png

  • 局部变量提取再赋值: 首先将变量声明移动到即将要提取的地方 image.png 其次将提炼的代码放到目标函数中 image.png 最后用新的函数去给outstanding变量进行赋值 image.png

关于 提炼函数(局部变量提取) 我在代码中是如何应用的

原来的代码

export function server2ClientNormal(datas) {
    const schemaVersion = datas.version.schemaVersion;
    const verNo = datas.version.verNo;
    const notifyStatus = datas.version.notifyStatus;
    const versionId = datas.version.id;
    const reason =
    datas.version.approveRecord == null
      ? null
      : datas.version.approveRecord.reason;
    const modifyTime = datas.version.modifyTime;
    const logRate = datas.version.logRate;
    const cachePlan = datas.version.cachePlan;

    const servers = datas.servers || [];
    //在函数中定义了一个大的对象,使函数变得冗余了,可以使用局部变量提取来改造
    const fields = {
        name: {
          value: datas.ruleGroup.name
        },
        description: {
          value: datas.toggle.description || datas.version.description
        },
        versionDescription: {
          value: ""
        },
        whiteType: {
          value: "phone"
        },
        whiteValue: {
          value: ""
        },
        publishTo: {
          value: publishTo
        },
        group_noun: {
          value: ""
        },
        logRate: {
          value: datas.version.logRate
        },
        cachePlan: {
          value: datas.version.cachePlan
        },
        servers: {
          value: servers.map(s => {
            return s.name;
          })
        },
        publishSettings: {
          value: {
            hasPlans: false,
            servers: [],
            publishStages: [],
            serverType: 1
          }
        }
      };
 }

改造后的代码

//提炼函数 ——> 局部变量提取 ——> 再赋值
//首先想要去提取fields这个对象为getFields函数
//观察这个对象需要的变量,提取到这个函数中
//最后将getFields函数赋值给fields变量
getFields = datas => {
    let publishTo = datas.version.publishTo;
    if (publishTo === "[]" || publishTo === null) {
        publishTo = [];
    } else {
        publishTo = publishTo.split(",");
    }
    const publishToFromToggle = datas.toggle.publishTo || [];
    const ids = [];
    publishToFromToggle.map(function (item) {
        ids.push(item.id);
    });
    if (publishTo.length === 0) {
        publishTo = ids;
    }
    const servers = datas.servers || [];

    return {
        name: {
            value: datas.ruleGroup.name
        },
        description: {
            value: datas.toggle.description || datas.version.description
        },
        versionDescription: {
            value: ""
        },
        whiteType: {
            value: "phone"
        },
        whiteValue: {
            value: ""
        },
        publishTo: {
            value: publishTo  //getPublishTo(datas)
        },
        group_noun: {
            value: ""
        },
        logRate: {
            value: datas.version.logRate
        },
        cachePlan: {
            value: datas.version.cachePlan
        },
        servers: {
            value: servers.map(s => {
                return s.name;
            })
        },
        publishSettings: {
            value: {
                hasPlans: false,
                servers: [],
                publishStages: [],
                serverType: 1
            }
        }
    };
}

坏味道之过长函数

以查询取代临时变量

百分之九十以上的场合,要把函数变短,只需要使用刚刚讲的提炼函数方法就可以了,找到函数中适合集中的部分,提炼成一个新的函数就可以了,但是如果函数中有大量的参数和临时变量,它们就会对函数提炼形成障碍,最终会把很多参数传递给被提炼出来的新函数,导致函数可读性降低,这时候可以用以查询取代临时变量的手法来消除临时元素。 以查询取代临时变量手法适用于提取那些只被计算一次且之后不会被修改的临时变量,被多次修改的变量应该将计算代码一并提炼到取值函数中,还适用于那种被很多函数当作为参数进行传递的变量。 image.png 这里有一个细节就是会先将basePrice变量的声明方式从var修改成const,这是很细节的一步,可以确定basePrice变量是否被多次赋值或者修改。 image.png image.png image.png image.png image.png

关于 以查询取代临时变量 我在代码中是如何应用的

对于刚刚改造后的getFields做进一步的改造

 // 以查询取代临变量,将刚刚改造的getFields函数中的上计算publishTo的部分提取出来
 // 这样也减少了getFields函数的代码,还可以对publishTo变量进行重复读取
getPublishTo = datas => {
    const publishTo = datas.version.publishTo;

    if (publishTo === "[]" || publishTo === null) {
        publishTo = [];
    } else {
        publishTo = publishTo.split(",");
    }

    const publishToFromToggle = datas.toggle.publishTo || [];

    const ids = [];
    publishToFromToggle.map(function (item) {
        ids.push(item.id);
    });
    if (publishTo.length === 0) {
        publishTo = ids;
    }
    return publishTo;
}

引入参数对象

刚刚用的以查询取代临时变量的手法可以用来消除临时元素,而引入参数对象则可以将过长的参数列表变得更整洁 image.png image.png image.png 创建一个合适的数据结构(对象、类) image.png range实例将取代operatingPlan.temperatureFlooroperatingPlan.temperatureCeiling参数 image.png image.png

image.png

image.png 在类中声明内部方法contains,用来封装外部逻辑 image.png 最后将三个参数变成了两个参数,将温度范围封装到了range类中,打造了一个范围类,每当代码中出现最大值,最小值这样的数字,我们就会考虑可以将其改为使用“范围”类,观察数字是如何被使用的,发现有用的行为,并将其搬移到“范围”类中,简化使用方法 image.png

总结

最大的收获莫过于感叹作者的谨慎,震惊于作者的重构思想,对代码重构的理解程度之深,虽然有时候作者真的过于小心了,但不可否认,这是一种非常正确并且必要的方式,在我们的日常工作中,因为我们了解自己的业务代码,所以我们可以稍微大一些步子前进,当遇到问题,可以回滚部分代码进行细查修改。
我只是阅读了这本书的很小一部分,上面的三个场景其实也是我们在日常开发中最常遇到的坏味道代码,如果想更深入的实战重构还是要细致的阅读下每种坏味道的处理方法,才能更好的更安全的去重构代码,愿重构路上的每一位勇士都能一往无前。