本篇主要介绍9种方法对函数进行重构,包括以下几个方面:
函数:
- 1 入参
- 2 临时变量
- 3 算法
- 4 结构
- 5 注释
- 6 返回值
方法:
- 1 提炼函数
- 2 内联函数
- 3 内联临时变量
- 4 函数代替临时变量
- 5 引入解释性变量
- 6 重复使用临时变量
- 7 禁止参数赋值
- 8 对象代替变量
- 9 替换算法
基础
基础方法强调将具有独立逻辑的代码封装到一个函数,并给函数起一个能表达其功能的名字。目的很简单,就是为了去掉“注释”。
1. 提炼函数
主要是针对那些函数体包含太多逻辑代码,可以考虑将其中具备一定独立逻辑的代码提炼成一个函数。这样做的好处是:(1)一个适当的函数名字可以省略一段注释,(2) 越小规格的分解,越容易将来被复用。
示例:
public int increaseSalary(Person person){
if( person.score > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
/**
* Print person information
*/
System.out.println("Person name: " + person.name);
System.out.println("Person age: " + person.age);
System.out.println("Person position: " + person.position);
System.out.println("Person salary: " + person.salary);
}
好的代码应该一目了然,如果通过函数名就能知道它的作用,那为什么还要加注释呢?!
public int increaseSalary(Person person){
if( person.score > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
printPersonInfo(person);
}
private void printPersonInfo(Person person){
System.out.println("Person name: " + person.name);
System.out.println("Person age: " + person.age);
System.out.println("Person position: " + person.position);
System.out.println("Person salary: " + person.salary);
}
提炼函数适用度极高。在项目初期,在需求驱动下,很少有人能顾忌到代码结构,很容易将一大坨逻辑塞到一个函数下,使得函数变得很臃肿。其中逻辑很难一眼看清楚,难免存在很多bug。在代码review的时候,就可以考虑用该方法为函数瘦身。这里有一点要说的是,特别对于国内的开发来说,如果不能合适的命名一个提炼出来的函数,那就不暂时不要动了。一个函数应该涵盖一个完整的功能。
2. 内联函数
内联函数概念通俗来讲就是把上一个方法提炼的函数塞回去。第一种方法好处有百般,为什么还需要第二种方法。这里有两种情况,第一种是有人过于喜欢运用第一种方法,把项目代码细分的过于琐碎。也就是发力过度,这时候就需要用该方法还原。第二种是我们发现一个项目组织的乱七八糟,我们想对它进行重构,但是它已经乱到无从下手了,这时候就可以先把零碎的函数全部揉成一个,理清逻辑,再做合适的拆分。
进阶
进阶方法是针对变量进行优化,进一步精简函数。
3. 内联临时变量
函数体中经常会出现大量的临时变量,其中有一些仅仅是为了记录某一计算结果。这种变量通常被赋值了一次。
public int increaseSalary(Person person){
double score = person.getScore();
if( score > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
这种情况,在项目中随处可见。个人理解什么时候我们该用这种方法:(1)这个变量赋值一次之后,没有再次赋值,(2)getScore方法代价极小,并且出现的次数并不多,至于多少算不多,需要自己把控。
public int increaseSalary(Person person){
if( person.getScore() > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
4. 函数代替临时变量
方法 3 中我们是直接用函数代替了一个临时变量,但有时候一个临时变量是通过另外两个变量计算得来的,有可能是两个成员变量。这个时候可以对它抽象出一个函数来在运用方法3。使用场景参考方法3.
public int increaseSalary(Person person){
double score = baseScoreRate * person.score;
if( score > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
临时变量的滥用,显然会驱使我们写出很长的函数。将临时变量替换为一个函数,一方面也是为了其他成员函数的使用。
public int increaseSalary(Person person){
if( getScore(person) > 90) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
private double getScore(Person person){
return _baseScoreRate * person.score;
}
5. 引入解释性变量
该方法可以说是对方法4的逆操作,从例子中就能感受出区别。
public int increaseSalary(Person person){
if((person.getCert().indexof("master") > 1) &&
(person.getAge().toInt() > 35) &&
(person.getScore().compare() < 10)) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
添加三个临时变量,使得后续逻辑更加清晰。代码怎么写,计算机都能读懂,重要的是,要让别人读懂。final变量可以限制该值只能赋值一次,合适的运用可以让编译器帮我们预防许多未知错误。
public int increaseSalary(Person person){
final boolean isCert = person.getCert().index("master") > 1;
final boolean isOlder = person.getAge().toInt() > 35;
final boolean isScore = person.getScore().compare() < 10;
if (isCertificate && isOlder && isScore) {
person.salary += 1000;
} else {
person.salary += 500;
}
}
6. 重复使用临时变量
这个意思指的是有一个变量被反复赋值,而它并不是循环变量也不是用于收集计算结果。看例子:
int tmp = 2 * ( height + width);
System.out.println( tmp );
tmp = heithg * width;
System.out.println( tmp );
这个现象在国内代码中出现比较多,看下开源项目,基本不存在这个问题。主要就是语言问题,找不到一个合适的词对变量命名,想到的第一个一般都是temp。
final double perimeter = 2 * ( height + width);
System.out.println( tmp );
final double ara = height * width;
System.out.println( are );
7. 禁止参数赋值
对参数赋值,主要是针对像JAVA这样按值传递的语言,在代码中对传进来值修改,可能会造成误解,因为实际上在函数内怎么修改都不会影响原先参数。为了避免不必要的麻烦,最好不要对形参直接修改。
public int getResult( int input) {
if( input > 2) {
input -= 1;
}
return input;
}
示例代码并没有什么意义,就是为说明之前描述的情景。这种方式最大的问题是混淆了按值传递和按引用传递两种方式。
public int getResult( int input) {
int result = input;
if( input > 2) {
result -= 1;
}
return result;
}
8. 对象代替变量
假如我们看到一个函数,里面糅杂着大量的变量和复杂的计算过程,我们可以通过创建一个新类,将其中所有变量作为新类的成员变量。
public class Calc {
public int calcSome(int one, int two, int three) {
int importantValue1 = (one * two) + getScore();
int importantValue2 = (two * three) + 1046;
if( importantValue1 > 100){
importantValue2 -= 1000;
}
/**
* and so on.
*/
return importantValue1 + importantValue2;
}
}
减少大量变量定义,可以提高代码可读性,将计算过程抽象出来,将来也可以方便的替换。
public class Calc{
public int calcSome(int one, int two, int three) {
Num num = new Num(this, one, two, three );
return new Num(this, one, two, three ).compute();
}
}
class Num {
private final Calc calc;
private double primaryNum;;
private double secondaryNum;
private double tertiaryNum;
public Num(Calc calc, double primaryNum, double secondaryNum, double tertiaryNum) {
this.calc = calc;
this.primaryNum = primaryNum;
this.secondaryNum = secondaryNum;
this.tertiaryNum = tertiaryNum;
}
int compute(){
int importantValue1 = (one * two) + calc.getScore();
int importantValue2 = (two * three) + 1046;
if( importantValue1 > 100){
importantValue2 -= 1000;
}
/**
* and so on.
*/
return importantValue1 + importantValue2;
}
}
9. 替换算法
以下代码还是经常能看到的,一个简单的多路选择过程,用下述方式和swith都可以实现,但是无论哪一种,都应该被避免。switch最大的问题就是重复,在面向对象语言有多态的概念之后,就应该尽量避免使用switch。
String foundSomeone( String[] things ){
for( int i = 0; i < things.length; i++){
if( things[i].equals("apple")){
return "apple";
}
if( things[i].equals("balana")){
return "balana";
}
if( things[i].equals("pineapple")){
return "pineapple";
}
}
return "";
}
重构后:
String foundSomeone( String[] things ){
List candidates = Arrays.asList(new String[] {"apple", "balana", "pineapple"});
for( int i = 0; i < things.length; i++){
if (candidates.contains(things[i])) {
return things[i];
}
}
return "";
}
上述9个方法针对函数全方位优化。方法终归是方法,如何使用得当才是考验。很多方法都是成对出现,显然存在矫枉过正的情况。那么怎么才能融会贯通呢?首先,熟记于心,其次带着9个方法去读开源代码,去修改自己代码。假以时日,这招亢龙有悔的悔自决就练到家了。