代码重构不是笑谈

简介: 代码重构不是笑谈

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

什么是重构?

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

什么时候重构?

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

一:立刻重构

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

二:排期重构

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

什么时候需要重构

红色:我有自己的想法
绿色:不需要解释的异味
  • 神秘命名
  • 重复代码
  • 过长函数
  • 过长参数列表
  • 全局数据
    • 变量放全局,各处使用随时赋值,永远不知道是哪个逻辑的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.封装变量

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

//旧代码
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);
}

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

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

Class Deparment


get manager(){
   return this._manager;}
set manager(arg){
   this._manager = args;}

客户端调用:

manager = aPerson.department.manager;

优化后:
Class Person

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

客户端调用

manager = aPerson.manager;

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

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 = "男";
    }
  }
}

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

管道符优化后

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

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;

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

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

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

//源逻辑
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);
  }
};

17.引入特值

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

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

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

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

19.移除标记类型

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

20.以查询取代参数

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

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

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

22.移动代码

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

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

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

🤏一点想法

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

⚠️些许告诫

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

补充

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

目录
相关文章
|
5月前
|
设计模式 数据处理 开发者
LabVIEW软件开发中的代码重构如何帮助维护代码质量?
LabVIEW软件开发中的代码重构如何帮助维护代码质量?
64 0
|
设计模式 SQL 算法
【Java设计模式 规范与重构】 六 代码重构小结
【Java设计模式 规范与重构】 六 代码重构小结
206 0
|
设计模式 安全 Java
【Java设计模式 规范与重构】 二 重构的保障:单元测试,以及如何提高代码可测试性
【Java设计模式 规范与重构】 二 重构的保障:单元测试,以及如何提高代码可测试性
174 0
|
运维 Java 测试技术
重构性项目如何回归测试?
重构性项目如何回归测试?
|
设计模式 Java 测试技术
代码重构:面向单元测试
重构代码时,我们常常纠结于这样的问题:需要进一步抽象吗?会不会导致过度设计?如果需要进一步抽象的话,如何进行抽象呢?有什么通用的步骤或者法则吗?为了保证直观,本文将以一个 “生产者消费者” 的代码重构示例贯穿始终。最后还会以业务上常见的 Excel 导出系统为例简单阐述一个业务上的重构实例。
代码重构:面向单元测试
|
设计模式 测试技术 程序员
代码重构该怎么办呢
《系统设计》系列
178 0
代码重构该怎么办呢
|
程序员
程序员如何做好代码重构?
代码重构重构就是在不改变软件系统外部行为的前提下,改善它的内部结构。重构不是重写,它们的区别你可以理解为,重构是修复代码,大框架不变。重写是扔掉原来的,重新设计框架。
219 0
程序员如何做好代码重构?
|
IDE NoSQL Java
代码重构终极指南!!
我们一直在寻找各种方法来清理代码、降低复杂性和改善功能。而重构为我们指明了前进的方向。
367 0
代码重构终极指南!!
|
算法 Java 容器
狗屎一样的代码!快,重构我!
狗屎一样的代码如何重构? 重构不止是代码整理,它提供了一种高效且受控的代码整理技术。
135 0
|
Java 关系型数据库
记一次代码重构
# 单一职责 ## 功能单一 功能单一是SRP最基本要求,也就是你一个类的功能职责要单一,这样内聚性才高。 比如,下面这个参数类,是用来查询网站Buyer信息的,按照SRP,里面就应该放置查询相关的Field就好了。 ```java @Data public class BuyerInfoParam { // Required Param private L
9423 1