您的位置:首页 > 编程语言

代码重构-改善代码设计(图文+代码)

2016-12-27 16:59 246 查看
最近工作告一段落,在朋友的推荐下,在看两本书,这里也分享一下给大家:
《敏捷软件开发》
《重构改善》
《人月神话》
《代码大全2》

如果有条件的话,建议大家多看几次,朋友说每次看都有不同的心得体会。这当然啦,随着自己的代码量增长,理解程度是不一样的。


重构第一步:为即将要写的代码建立一套测试环境

对于测试来说,这里是跟代码挂钩的,我们说这是白合测试。上面这句话呢是老外说的,其实我做开发以来,只以数据库写过测试。不过呢,有些公司是要强制写单元测试的。对于这个观点,保留意见,可以做,可以不做。简单的话就不需要先建立测试环境了。


把功能模块化

在首次写代码的时候,一般来说,我们都是按照自己的思路,业务逻辑去写。这有可能会导致一个方法很臃肿,当然,这并不影响使用,对吧。但是,当我们扩展的时候呢,我们就需要进行重构了,把功能模块化,独立出来。那里要那里就调用就可以了嘛。

比如说:我现在有一个方法直接输出用户的信息,首次不需要扩展的时候:直接把用户名,用户的邮箱,用户的消费金额,用户的平均每天的购买次数。我们直接把这些信息封装到一个方法里头,直接输出就可以了。但是,当我们要以另外一种形式输出呢,比如说要构造html的形式来显示给用户,对吧!这就不好搞了吧,编写另外一个html输出方法,这样子,原来的数据什么之类的又要准备一次。所以呀,我们可以把获取名字,获取邮箱,计算平局消费次数这些独立出来成一个方法,要用的时候 直接get就可以了。这样子呢,功能就单一模块化了,谁爱谁用呗,对吧!


建立测试体系

在我们国家,百分之八十以上的人写程序是不编写测试代码的。因为这会带来更多 的工作量。那到底是写还是不写呢?

首先我们把工作分成:业务逻辑的需求分析、专业知识的准备(调研)、程序设计、编码、解bugs。如果说,最后一个的时间比前面所有的时间长,那么你有必要去写测试代码了,那样子会减少你的bug数量,减少开发的时间。另外一个是程序的设计,当你编写测试代码的时候,你的逻辑是很清晰的,还有一个好处就是对于大项目来说,有很多测试是重复的,所以使用代码或者脚本来自动化测试是一件很省时间的事。


什么时候进行代码重构

1、重复的代码:俗话说,事不过三,如果我们发现一样的代码写了三次,那么我们就必须要对代码进行重构了,也就是说,这代码应该被重复使用。

2、方法过于臃肿,如果一个方法太长了,太多了。那么就要像前面说的那样对方法进行切割,功能继续细划。对要完成的事进行分流即可。

3、参数过多。参数过多的话,可以把要传的参数封装成一个Bean类对象,直接传进来即可。

4、如果类太大了,也最好进行拆分。还是按照面向对象的思想,相同的东西进行抽象,独立到父类中。针对性地分析类的作用。


对重构做好记录

在做重构时我们要记录什么东西呢。

1、名称:创建一个重构的词汇表

2、名字的概要:对前面名字的诠释

3、动机:你为什么需要这个重构呢

4、做法:你是怎么进行重构的

5、范例:以一个十分简单的例子来说明这种重构是怎么运作的

比如说接下的:重新组织函数


重新组织函数

名字:重新组织函数

名字的概要:函数里头有些代码可以重新组织在一起并且独立出来

动机:对于函数重构呢,是我们常用的重构手法之一。为什么要对函数进行重构呢?第一个方面是你觉得函数太长了,那到底什么函数叫做长函数呢?这个没有定义哈,我觉得,只是你能够把一段代码独立出来,并且能给它的逻辑所实现的功能起一个匹配的名字就可以独立出来了。经过这样的分块,函数的体积会变得比较小,而不显得那么臃肿。

做法:通过阅读代码,理解代码的逻辑。尽可能地把代码的功能模块化。当我们把一段代码单独出来的时候 ,那么它就负责做一件事。当然啦,也为了可以重复使用。在这个时候 ,我们要注意变量之间的引用,不过不需要担心,大多数的开发工具会帮我们搞定的。只要我们选中代码块,然后Extract method一下,起个名字,就可以独立出来咯!

范例:
/**
* 这个例子不一定好哦,只是为了说明而已
* <p/>
* 说先这个类用于负责完成加减乘除的动作
* <p/>
* 通过传入符号来进行运算
*/
public void calculation(int numA, int numB, String operator) {

if (!("+".equals(operator)) && !("-".equals(operator)) && !("*".equals(operator)) && !("/".equals(operator))) {
throw new IllegalArgumentException("Sorry,your operation not right!");
}

if (operator.equals("/") && numB == 0) {
throw new IllegalArgumentException("If your operation is divider,numB can not be zero.");
}

int result = 0;
switch (operator) {

case "+":
result = numA + numB;
break;
case "-":
result = numA - numB;
break;
case "*":
result = numA * numB;
break;
case "/":
result = numA / numB;
break;

}
Log.d(TAG, numA + operator + numB + " = " + result);

}


那么在这个方法里呢,有一段代码是用于检测运算符的。那么我们可以独立出来成一个方法叫checkOperation,如下:选中要重构的代码志块-->右键 Refector-->Extract-->Method



于是呼呢,就会弹出一个框框啦,输入新函数的名称即可独立成一个新的函数(方法)



private void checkOperation(int numB, String operator) {
if (!("+".equals(operator)) && !("-".equals(operator)) && !("*".equals(operator)) && !("/".equals(operator))) {
throw new IllegalArgumentException("Sorry,your operation not right!");
}

if (operator.equals("/") && numB == 0) {
throw new IllegalArgumentException("If your operation is divider,numB can not be zero.");
}
}


好啦,到这里的话,这个重构方法的记录就写完啦。


内联函数

名称:内联函数

名称概述:对于内联函数来说,就是在函数调用的内部插入函数的内容,直接移除该函数。类似于回调里头要传一个接口,不同的时,函数里头要传值,但这个传呢,由另外一个函数运算所得。那么,如果这个运算的函数比较简单,是可以单独地写到要传值里。

动机:其实这里这个内联函数和前面的重新组合函数是相反的。前面是抽取,而这里则是把简单的合到一起。这样的目的,也是让代码更加清晰,不多的话,直接看就可以了,没必要跳转到对应的函数体进行查看对应的逻辑。

做法:找到对应的函数体,然后把函数内容替换调用点。编译,运行,进行测试。

范例:(例子来自己书中)这种情况比较少见哈,嘻嘻!毕竟方法是自己写的嘛。
public int getRating(){
return (moreThanFiveLateDeliveries())?2:1;
}

private boolean moreThanFiveLateDeliveries() {
return (numberOfLateDeliveries > 5);
}


重构以后呢:
public int getRating(){
return (numberOfLateDeliveries > 5)?2:1;
}


按照上面这个套路是总结哈,每个人都应该有一套对应的重构方法,但又很多人都是相同的。

这后面的话,总结一下一些常用的重构方法哈

1、在不影响阅读性的情况下,尽量地减少行数,合并代码:
double basePrice = anOrder.basePrice();
return (basePrice > 1000);


那么这个的话,就是可以上面这个basePrice不用复制出来一个变量了。直接是:
return (anOrder.basePrice() > 1000);


这样子就可以合成了一行,并且不影响阅读性,一看就知道的嘛。这种是比较常见的哈!

2、以查询取代临时变量(Replace Temp with Query)

这个的话,很简单,这样做理由是,抽取出来的函数可以给别的函数调用。这样子,从某种程度上说,是减轻了函数的长度。

直接使用前面的Extract--Method的方法就可以进行重构,起个直白的名字就好。

3、引入解析性变量

对于前面的重构,对吧。这个形式呢,是为了增加阅读性。为什么呢,还是看例子吧:
if((platformtoUpperCase().indeoxOf("MAC")>-1)&&(Browser.toUpperCase().indexOf("IE")>-1)&&(wasInitialized()&&resize>0)){
///do some things here...
}


这样子读起来呢,是很累的,阅读性特别差,又长又臭是吧。那么我们怎么重构比较好呢?
boolean isMacOS = platformtoUpperCase().indeoxOf("MAC") > -1;
boolean isIEBrowser = Browser.toUpperCase().indexOf("IE") > -1;
boolean wasReiszed = wasInitialized() && resize > 0;

if (isMacOS && isIEBrowser && wasReiszed) {
///do some things here...
}


那这样子是不是一目了然了呢?不过呢,如果你能在写的时候 就这样子写的话,就已经很OK了。所以的话,这个一般不需要重构,你看到就可以整理一下嘛,就这样子就差不多了哈。

4、分解临时变量

这种写法呢,是不对的:
int width = 9;
int height = 19;

double temp = 2 * (width + height);
Log.d(TAG,"perimeter == " + temp);

temp = width * height;
Log.d(TAG,"area == " + temp);


这样写为什么不对呢?一个临时变量多次使用的话,又不知道什么意思,容易出现错乱。

所以呢我们要分解临时变量,该写的还是该写。
int width = 9;
int height = 19;

double perimeter = 2 * (width + height);
Log.d(TAG, "perimeter == " + temp);

double area = width * height;
Log.d(TAG, "area == " + temp);


这样子的话,是不是感觉这个世界又美好了一些呢?

5、不可以对函数的参数进行赋值。为什么呢,当一个值从函数调用的地方传进来的时候 ,它就是对应着一个变量了。这个变量了,在这个函数走完,都不能变。否则它的意义就可能变了。

6、搬移函数

在类与类之间呢,可能存在一些函数调用。有静态的,有一些需要创建对象的才能调用的方法。那么如果这个函数单一使用的话。我们可以通过面向对象的方法来对函数进行搬移。

7、对类的提炼

类于与之间呢,面向对象的特征中有多态和继承。我们要对类进行抽取,抽象出共同点。然后整理类与类之间有关系。尽量重复代码的使用即可。

8、如果判断的条件太长的话,那么可在把判断的条件独立出来一个函数,这个函数通过
c2f3
传对应的判断阐述进去,然后进行判断,返回boolean类型的结果即可。这样子的话,可以减小这个条件的体积。

9、合并条件中重复的语句

我很过很多小伙子的代码,的确有这个问题哈。是这样子的:
if(isSpecialDeal()){
total = price * 0.97f;
send();
}else{
total = price * 0.95f;
send();
}


这里的send呢是一样的, 所以我们可以把它扔到最后去,对于这种情况,在Switch--Case也要考虑一下是否有这样的情况哈,不过对于Case过多的话,就防止提出来出错了。因为不是所的有情况都执行得到哦。那上面这个例子怎么改呢?

很简答哈;
if(isSpecialDeal()){
total = price * 0.97f;
}else{
total = price * 0.95f;
}
send();


这种是对于新手来说是比较见的哦。对了,这本书的话,真的基础到很基础的。很适合新同学们看哈...

当然啦,书中也兴了一些很垃圾的代码,这看起来基本不会是人类写出来的嘛。

10、异常统一处理

这个我喜欢哈,嘻嘻。通过一个错误码,然后呢,去输出对应的异常。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息