代码重构-找出问题代码

1,561 阅读11分钟
原文链接: www.jianshu.com

写在文前:大部分程序员都能写出计算机可以理解的代码,唯有优秀的程序员才能写出让人容易理解的代码

最近几个月被分配了一个项目重构的任务。说实话,项目重构甚至包括不少的业务代码的重写真的是耗时耗力的大工程。

有人可能会问,既然现有工程能正常的跑,为什么还要费大力气去重构和重写呢。究其原因还是为了以后维护起来更加方便。

就拿我这边的老代码来看,由于经过很多代程序员的辛苦耕耘的原因,使得程序能正常运行,同时带来的另外一个问题就是乱,特别乱

文件命名之类这种就不说了,不管如何你最后还是能通过最开始的入口一层层的跳转来找到。

重点是有些业务逻辑配合你那不知所云的函数名,看起来真的是想哭。曾经有一个模块出现了个bug需要定位,碰巧负责该模块的人请假了。于是我和leader两个人花了大半天的时间才在几千行代码中定位出那个bug。而实际上那个bug并不复杂。

tip:本来会优先讲述一些可能需要重构的问题代码。

tip2:代码重构的博文之后还会有几篇

重复代码

问题代码排行榜之首自不用说是重复代码了。 同样的逻辑出现在各个函数的中,看着就烦。

场景一
多个类中出现相同逻辑的函数

这种情况是比较场景而且也容易解决的,重复的代码抽离出来,放到公共类譬如Utils中

场景二
继承与同一个父类的多个子类含有相同的逻辑

将这部分相同的逻辑代码推入到父类中。最常见的就是BaseActivity及子Activity,很多共有的逻辑都可以写入BaseActivity中。

总之,重复代码应该是最好解决的,只要保证大部分业务代码不要重复出现即可。

过长的函数

简单点说就是那些几十近百行的函数方法。 过长的函数很有可能会有如下问题。

  • 过多的局部变量 (变量越多意味着不可控因素越多)
  • 过深的缩进层级 (if,while,接口回调过多会导致这个问题)
  • 过于复杂的业务逻辑 (不容易理解,也容易出错)

譬如

//支付逻辑
private void goOrder(){
    XXX   // 一段逻辑,获取支付方式
    xxx   
    xxx
    xxx   //又是一堆代码,支付订单
}

更合适的方式应该是

//支付逻辑
private void goOrder(){
    int orderType=getOrderType();  //获取支付类型
    payOrder(orderType);              //支付订单
}

简短而又意义充分的函数更容易让人看懂。在提炼代码的时候需要注意,你提炼出来的这一段代码要有充分的意义。比如获取某个数据,比如绘制底图,比如创建一个必要的对象。

同时需要注意新函数的命名,函数名要能足够表达这个函数体的功能。一般简单的函数名多以动词+名词的方式表示。诸如 createBitmap 之类的。

如果函数名过长,则可以使用简写,但要注意,简写的时候不能抹去单词的意思。我所负责的项目中就遇到把 startActivity 写成 startAC。初看时完全不明所意。真要用到简写,还不如写出 goActivity这样。

过于庞大的类

比起让你看一个几千行代码的类,我相信你更愿意看几个几百行代码的类。

有的时候想通过一个类来解决问题是不合理的。 一个类过于庞大势必会出现很多问题,

  • 过多的全局变量,为程序增加了许多不安定的因素
  • 过多的函数,阅读起来费劲,文章开头提到的就是因为类过长导致的
  • 代码耦合性变强,很多本该公用的代码都挤在一块
  • 维护难度,修改难度不同程度的变高

比较合适的解决方式就是分拆类,将一个庞大到5000行代码的类拆成多个功能不同的类。注意分拆出来的类必须要有自己核心的逻辑。或负责处理数据,或负责展示数据。

当然分拆类势必会带来一个不好的后果,那就是代码总数会增加不少。这些增加的代码大都是为了维持几个类之间的联系。

虽然会增加一些代码行数,但是比起看上去简洁的代码逻辑和大大减少的全局变量。多些代码无关紧要。

过长的参数列表

上一条提到过多的全局变量容易引入不可察觉的bug,那变量只能通过参数的形式传入到函数中。一般情况我们也都是这么做的。

但这也可能会导致另外一个问题,如果一个函数需要大量的参数才能执行,这个时候你不得不写下长长的参数列表。过长的参数列表虽然不太会引入bug,但却难以阅读。

这个时候你仔细观察一下函数便会发现,这个函数所需要的参数大都可以在一个对象中取到,所以你只要传入对应的对象即可。

"什么?"你说你的函数所需要的参数不能直接从对象中取。 这个时候,要么你自己组装一个对象,要么就是你的代码结构存在某些问题。

耦合性太强

当你需要添加一个新功能的时候,发现除了添加功能的代码的同时还需要修改其他多处代码。这就意味着你的代码的耦合性太强了。

这种耦合性强的问题大致分为两种

  1. 每添加或修改一个功能,都需要修改某一个类中的多个方法。
    这个时候就应该把经常改动的方法抽取出来
  2. 每添加或修改一个功能,都需要改动多个类中的多个方法
    同样可以把经常改动的方法单独抽取出来放置到新的类中。

过多的实体类

在开发过程中,经常需要定义实体类来存放对象。尤其是在网络请求的时候,几乎是一个接口对应一个实体类。

接口繁多再加上本地代码需要的实体对象,你会发现实体类代码的体积占比过重,这个时候你就需要做出调整。

仔细观察你会发现有不少接口的返回中你只需要几个重要的字段, 诸如id,name,time之类的。

只不过可能事先没有与服务端商议好,然后就会出现这样的情况

A接口返回 id,name,time B接口返回tid,tname,ttime

实际上A,B两个接口返回的数据结构是一模一样的。

这个时候就需要用到 @SerializedName 这个注解,这样就不需要分别为A,B两个接口定义两个实体类。只要如下定义即可

@SerializedName("tid")
private String id;
@SerializedName("tname")
private String name;
@SerializedName("ttime")
private String time;    

另外还要种情况,譬如 A接口返回 id,name, B接口返回id,name,time 这样的情况,可以只定义一个实体类,包括id,name,time字段即可。

还有一点,我在定义实体类的时候,会细分成基本实体类,和接口返回实体类。 这样看上去更加清晰一些。

别留下无用的代码

日常工作中经常会再别人的代码中看到一大段注释的代码,旁边还写着将来可能要用。然而实际上这些注释掉的代码很大程度上都是用不到的。

为什么会出现这样的情况,那是因为很多人不愿意删去那些自己费心费力写出来的代码,同时心里抱有一丝期望,万一用到呢。

先不说用不用得到,就我个人感觉,再某些代码中看到注释掉的代码比正常运行的代码还多,头都大了。

既然程序能正常运行,那就说明注释的代码毫无作用,留着还会影响阅读。 除非这些注释的代码在可预见的未来能用的上。否则就应该删去。

就算以后突然要加上这个功能,那你到时候再加上就好了,反正写过,再写一次也没多大问题。

别为不可预见的未来预留入口

比较经典的场景就是再定义接口的时候,预先定义了多个方法。 典型的列子就是 列表诸如RecyclerView 的item事件。

定义接口方法的时候预先定义了 itemClick,itemLongClik,xxx 等等 殊不知有些方法根本不会用到。

别人问起来说这个实现的方法为什么没有内容体,你总是说这个留着可能以后用的到。但是这个以后可能等到项目倒闭都不一定等的到。

所以不要为不可预见的未来留接口。当然如果需求已经加在任务时间线中的这种就不一样。这种情况下,你必须得为以后的需求留好坑位。

去除/合并一些不太重要的类

如果一个类的作用可以被代替,那你就可以考虑是否把他合并到别的类中。

比较常见的情况是,我们通常会定义比较多的utils类,比如StringUtils,ToastUtils,ScreenUtils之类的帮助。但是这些类中的方法数又不多,这个时候可以考虑合并成一个utils,诸如CommonUtils来代替上述的多个Utils。

过多的注释类

我认为代码更多的时候是写给人看的,为了让别人看懂,取一个合理易懂的名字十分重要,但有的时候因为没有合适的名字或者名字过长又或者名字已经无法表达这个函数的意义,那么这个时候你就需要添加注释。

那么怎么样的注释合适呢?

错误示范

//这是id
private String id;
//这是名字
private String name;

这种显而易见的变量如果在加上注释,那就有点像裹脚布越裹越长。

错误示范2

//获取用户信息
private void getUserInfo(){}

这个错误跟上面那个一样,只要有合适的函数名,根本不需要注释

错误示范3

/***********/
/2017.9.1   添加了xxx
/2017.9.10  修改了xxx
/***********/

有的人习惯在类或者函数名前加上这些修改信息,我也在项目中碰到过这些注释信息。 实际上这些注释毫无用处,写的多了还会影响阅读。况且现在还有svn/git 这样强大的版本管理工具,谁提交的代码,修改了什么一目了然。

正确示范

//登录的入口,code==0 loginByPhone;
//code==1 loginByQRCode ;code==2 loginByLauncher

private void login(int code){
    loginByPhone/loginByQRCode/loginByLauncher
}

上述示范中,单看函数名你只知道是登录操作,但里面的参数code所代表的意义却不明,这个时候你就需要加上一个注释来解释不同code值对应的意义。

实际上这个示例也不太合适,更合适的方式应该是定义几个常量,根据常量来判断具体执行的流程。

列举这个示例的目的是为了说明,当一个函数名无法完全表达清楚函数的意义,而且这个函数容易出现理解上的偏差时,就需要加上注释,而且注释最好写的完整点。

正确示范2

可以参考源代码的注释,Android源代码的注释大都会将函数的描述的比较清楚。

诸如什么情况下可以调用函数,什么情况下不能调用。返回值又都代表什么意义。这样的注释看起来就很舒服。

写在文末:如果读者朋友有什么问题或者意见欢迎在评论里指出.