酷勤网 – 程序员的那点事!

当前位置:首页 > 管理 > 软件工程 > 正文

软件重构

浏览次数: AndroidKy 2015年09月23日 字号:

重构这个话题是老生常谈的了,无论对于C、java亦或Python程序员来讲,只要项目有一定的代码量,重构就是无可避免的。正好这段时间我正在给一个android应用项目(下文统称项目X)做重构,这个项目原本是由一个完全不会写代码的人写的,可以说项目没有任何可读性,逻辑也没有很清晰。本文我会结合自己的实践和一些参考资料谈谈我对重构的一些理解。

什么是重构?

重构是在保证不改变外部行为的前提下,对内部结构进行改变,使之易于修改和理解。 ——————Martin Fowler

换句话说,重构就是保证我们的程序对于外部使用者来说是一致的,但是内部的代码做了优化。

为什么要重构?

这个问题其实很简单,就是代码写的不好。当然了,代码写的不好也是不可避免的,再nb的程序员写出来的再nb的项目也会有可优化的地方,Linus Torvalds写的项目会不会一点问题都没有?不会。有时候可能一个变量含义不清,一个函数的功能不明确,类定义有部分耦合,Linus Torvalds不是神,这些或大或小的问题总会出现的。

需要重构的情况和解决方法?

下面就简单说说那些情况下我们就需要重构代码了,你也可以对照着这些情况重新审查自己的代码是不是有类似问题。

违反了基本的代码规范

基本的代码规范包括但是不限于如下:

  • 命名采用驼峰式,命名是有意义的,而非类似temp、data之类,private和protected命名前加m,静态变量加s,静态常量全部大写等等,这里不过多写了
  • 魔鬼数字,将魔鬼数字定义为静态常量,并给他详细的注释,这个习惯一定要保持

可能有人说这些不属于重构范围,因为代码逻辑根本没改,但是,一个坏的变量命名和一个好的变量命名给维护的程序员的感觉是完全不同的,好的命名根本不需要注释就能知道这个变量是做什么的。

重复的代码

这个我觉得是一个最显而易见的问题,一旦你发现有段代码是复制粘贴到另一个类中的时候你就应该想想,怎么样能够复用这段代码,Don't Repeat Yourself!

怎么样能复用同段代码,最简单的就是抽出来作为一个公共静态方法,但是很多时候这些方法不应该是静态的,这时候可以建一个helper或者delegate之类的类,两边都用这个代理函数去处理同一个逻辑。当然,我这边只是一个例子,并没有包含大多数情况。

而如果两个函数基本类似,仅仅是其中的一些变量不同,那就把这两个函数合并,通过重载的方式,以函数参数来区分。

冗长的子程序

比如在java里面,一旦你发现有个函数特别长,那肯定是不正常的(这里说的有点绝对,不过基本上是这样)。在google针对java的代码规范里有这么两条:

  1. 保证函数的功能单一性
  2. 一个函数的代码不要超过X行(这边的数字每个公司可能不一样),我所在公司规定是不超过一个屏幕,大概30行左右。

依照这个规范来讲,函数太长应该这两点都不会符合。

解决方法就是分拆函数,把过长的函数分拆为几个函数,每个函数的功能保证单一性,并为之取一个功能明确的函数名。如果几个函数都是逻辑类似的,你也可以单独拎出来作为一个类处理。

我在给X项目做重构的时候就遇到一个上传任务单这种函数,其实每个任务单表在后台会包含若干表,由于X项目没有接口层,所以客户端都是直接与后台数据库接触的,这样,上传任务单的一个功能就可以分拆为几个功能:上传任务单的人员信息,上传任务单的环境信息,上传任务单的参数信息等等,分拆为若干函数之后整合为整个上传任务单函数(这个函数里仅仅包含分拆的几个函数),这样每个函数的行数保证了不超过一屏幕。事实上,这样分拆的好处不仅如此,因为其他的上传功能也会交叉的有上述分拆函数,所以也解决了代码复用问题。

循环过长,嵌套太深

我认为没有程序员会喜欢看超多的if else,动辄十几个大括号的嵌套可能会让你很有成就感,事实上,这样的代码不可读且不好维护,此时的你成就感爆棚,一个月后呢?一年之后呢?我觉得,好的代码应该是一年之后你再次看到自己写的代码的时候,虽然不知道是自己写的,但也同样容易修改维护。

说到大括号的嵌套,我觉得Python程序员可能会吐,因为Python不是通过括号来嵌套代码的,而是通过4个空格,一旦层次太多...那画面真是...

来看两段简单的java代码

//代码1 public void test(String s) {     if (!TextUtils.isEmpty(s)) {         // do something     } }  //代码2 public void test(String s) {     if (TextUtils.isEmpty(s)) {         return;     }     // do something } 

这是一个防止if else过多的小技巧,就是把一些特殊情况先判断出来,不符合条件的就return,也就是说一旦知道了答案之后就立刻返回,而不要再去做赋值或者判断,这样一方面可以提高可读性(用语言翻译过来就是,如果不满足XX条件,这个函数就返回),另一方面扁平化了代码,避免了无休无止的大括号。

还有一个防止if else太多的方法,就是使用多态,太多数的if else都可能转换为比如策略模式,或者其他的多态方式。我这边不太清楚如何表述,就贴一段实例代码吧。

现在我要一个汽车类,提供一个跑的方法,怎么写?

一般的我们会定义一个汽车类,然后提供一个run()方法,根据传入的不同的参数来选择run()的逻辑,就像下面这样:

public class CommonCar {     public static final int BLUE_CAR = 1;     public static final int RED_CAR = 2;      private int mCarType;      public CommonCar(int carType) {         this.mCarType = carType;     }      public void run() {         if (mCarType == BLUE_CAR) {             // blue car do something         } else if (mCarType == RED_CAR) {             // red car do something         }     } } 

执行的时候就这样:

CommonCar car = new CommonCar(CommonCar.BLUE_CAR); car.run(); 

我们把内部的逻辑隐藏起来,外部调用只需要传入一个类型即可,代码看上去真不错。

这时候问题来了,如果我们还需要加一个汽车怎么办?就需要修改CommonCar类,加一个类型,加一个else if,这就和设计模式中的对修改关闭,对扩展开放的原则不符了。

为什么要有这个原则?个人理解是 1. 修改是成本比较高的,因为修改的人必须看懂以前的代码 2. 修改总是会产生bug,这是不可避免的 3. 维护成本也比较高,因为查询bug来源的时候总是要把无关的逻辑都检查一遍

怎么解决?

上文说了if else是可以用多态来代替的,我们就可以定义个car接口,用接口把汽车的行为特性定好:

public interface Car {     void run(); } 

然后写两个实现类:

public class BlueCar implements Car {      @Override     public void run() {         // blue car do something     } } 
public class RedCar implements Car{      @Override     public void run() {         //red car do something     } } 

外部调用就这样:

Car car = new BlueCar(); car.run(); 

这样有什么好处?

  1. 我们把if else去掉了
  2. 如果再加其他汽车,我们不需要改原来的代码,只需要再扩展一个汽车类即可
  3. 如果新的汽车出问题了我们不需要检查蓝色车和红色车的代码,直接看新的汽车类即可

如果汽车种类太多,构建频繁的话考虑使用抽象工厂

这部分的内容其实还牵扯到了其他重构的知识点,放在这里也是给大家推荐这么一个一个重构if else的小技巧

修改影响范围

有时候你修改一句代码,发现N个类报错,其实这个很大程度上也是因为代码不合理,这个也没有绝对,毕竟如果你修改的代码是比较底层的,那么影响的类必然会很多

同时使用的关联数据没有组织起来

说到这个点,就会想到刚才的参数太多的问题,其实如果这些参数是属于同一个组织的,那就把它们写成一个类,这个比较容易理解,不赘述了。

成员变量的访问权限

能private的变量不要设置protected,能protected的变量不要为public,保证封装的可见与不可见性,一般来说成员变量都是私有的。

类的构造方法

尽量保持类的构造方法简单,要不然new 对象的时候花费的成本比较高,如果真的是初始化的时候需要做很多工作,考虑把这些工作移交给factory或者creator去做。

类的内聚性差

什么是内聚性,就是说这个类的职责功能太多,而一个类只需要完成一件事就好。

解决这个问题也是和函数一样:分拆。不过类的分拆不像函数那样简单,不过你可以有这个思维,就是每个逻辑上的功能分为一个类,给类取一个形象的名字,至于分拆之后的高内聚、低耦合属于设计模式方面的东西,依情况而定,这个不是一朝一夕能理解的。

X项目有个功能是AGV模拟小车,原来代码把小车的UI、动画、服务器即时通讯、解析数据、分发事件所有的东西都放在了一个类里,导致类代码太多,一个类的职能太多。所以我把小车UI相关的代码封装了一个小车类,提供给外部类小车的前进后退旋转等几个基本的API,服务器通讯分给了通讯类,解析数据和分发事件放在了事件处理中心类,其实这个过程没有用到太多设计模式的东西,仅仅是把类分拆而已,但是这样重构之后代码明显会好很多。

要说有模式在里面的话也有,一个是简化了的工厂模式,考虑到可能会有不同的小车,所以设计为不同的工厂实例会创建不同的小车,还有代理模式,也就是把AGV地图的动画交由小车去执行,可能装饰器也勉强算一个,也就是把实际坐标包装为了规定的1280*720分辨率下的坐标

类的接口层次不同

这个问题似乎比较抽象,一般来说,在逻辑中不应该是这个类的接口(可能是它的子类才应该拥有的)就不应该给它,仅仅是为了编码能够通过测试就做了这样的修改是不可取的,结果会导致代码层次不清,难以维护。

比如如果你发现一个类的所有子类调用了同一个一样的函数,那可能这个函数是属于父类的,考虑把函数移到父类中。

这个问题其实也是很棘手,有时候不是因为我们不知道这个约定,而是知道了这个规则也不知道该怎么处理,我们分不清楚这个接口应该是属于这个类的还是它的子类。所以,大概这个问题还是需要积累经验的。

参数太多

经常会发生这样的事情,一个函数,尤其是逻辑上底层的函数,一般都会有十几个甚至更多的参数,幸运的是,有几个设计模式是专门用于对付这种问题的,当然,只能解决部分问题,那就是建造者模式。我们可以看到Android的AlertDialog这个类,个人觉得比较典型,AlertDialog拥有很多参数:标题、消息体、内容布局、OK按钮、取消按钮等等,但是它在类中有一个静态内部类:Builder,通过Builder,构建一个AlertDialog对象就变得简单很多,而且可以根据不同的需求动态添加参数,AlertDialog的代码就不贴了,感兴趣可以去找下看看。

一般我会使用建造者模式重新写一个对话框,X项目也是,用于提供风格一致的对话框。

写在最后

重构的话题还是比较大的,文章里面的东西不可能全部概括,总之平时要保持一个良好的编码习惯和一个写文档的习惯。

写完收工。

无觅相关文章插件,快速提升流量