重构36计

来源:互联网 发布:天天模拟器mac版 编辑:程序博客网 时间:2024/04/30 02:57

转载于:http://blog.csdn.net/m13666368773/article/category/1126930

重构,其实很简单,它的目的就是让程序变得更容易被理解,更具有可维护性,结构更合理。重构应该是我们平时写代码过程中必不可少的一部分,比如给函数起了一个更好的名字、把大函数拆分成几个小函数等都属于重构。重构的经典书籍包括Martin Flower的《重构-改善既有代码的设计》、Joshua Kerievsky的《重构与模式》,本系列的所谓36计是我多年来使用最为频繁的重构策略和编码准则,有自己总结的,也有书上提到过的,希望对大家能有所帮助。

第一计:参数列表对象化

公有函数的参数应尽可能保持不变,因为很多地方都会调用它,修改参数后需要修改它的调用处,另外,它的参数列表不宜过长,数量尽量保持在5个以内,长参数列表会增加该函数的调用难度。对于参数较多或者参数经常变化的公有函数,较好的办法是引入参数对象,即该函数的参数只有一个,它就是参数对象,具体的参数都在该对象中声明,为函数引入参数对象有以下几个好处:

1、保持函数接口的不变性,修改函数参数只需修改参数对象中的成员变量。

2、调用方便,调用方不用再关心参数的顺序。

以下代码片段是一个添加用户函数声明:

public long insertUser(String name,int age,String email,String address,String phone,String birthDay)

每当添加或删除用户的字段后都要修改insertUser的参数列表,调用者也需要修改,而且参数较多时,不容易记忆。

以下是引入参数对象后的形式:

public class UserParam{  public String name;  public int age;  public String email;  public String address;  public String phone;  public String birthDay;}public long insertUser(UserParam user);

第二计:条件运算符赋值代替if else赋值

对于根据条件为变量赋值的情况,可以有两种方式,一种是通过if-else:

int value;if(condition)  value = 1;else  value = 2;

另一种是通过条件运算符:

int value = condition ? 1 : 2;

第二种方式明显要比第一种方式好,但是很多人却钟爱第一种方式,可能是if-else习惯了。

第三计:节约使用系统资源

即使在写代码时,我们也应该养成“节俭”的习惯,不要随便浪费系统提供的资源,对于那些较占用空间、影响性能的对象,应该直到真正要用的时候才创建或者初始化,因此在提供这些对象的函数实现中,尽量采用如下形式:

// 管理数据库连接的类public class DataBaseConnectionHolder{  private Connection conn;  public Connection getConnection(){     if(conn == null){       conn = new Connection();       conn.init();       }     return conn;   }}

另外,我们可以通过引入缓存机制(如对象池)来充分利用系统资源,可以参看这篇文章:GoF著作中未提到的设计模式(5):Object Pool

第四计:为接口引入抽象版本

在声明一个新的接口时,不能保证该接口不会被修改,有可能会经常修改它,每一次修改接口都要修改相应的实现类,如果某个接口是公共库的一部分,那么修改接口的代价是较大的,用到该接口的所有程序都需要重新修改、编译...,通过为接口引入抽象版本可以解决这个问题,例如为下面的接口增加一个抽象类:

public interface Widget{  public void draw();  public void layout();  public void invalidate();  public void show();}

public abstract class AbstractWidget implements Widget{  public abstract void draw();  public void layout(){};  public void invalidate(){};  public void show(){};}

这样Widget的实现类可以直接从AbstractWidget继承,如果要修改Widget接口,则只需要修改AbstractWidget即可,对于其他实现类没有影响。

第五计:消灭魔法数

编程新手一般都会直接将表示类型或状态的数字直接写在处理逻辑中,代码的作者能明白该数字所表示的含义,但其他人读到这段代码时就很有可能看不懂了。即使代码的作者再过一段时间来看这部分代码,也可能会忘记该数字的含义,而且,当我们要修改魔法数的值时,过程是很繁琐的,很有可能会有所遗漏,所以,最好的办法是彻底消灭程序中的所有魔法数,通过常量定义、枚举等方式来避免魔法数的出现。

第六计:使用断言、异常确保实现的正确性

使用断言的目的是告知其他程序员代码中某处必须要遵守的规矩,它是debug版本中的一种确保程序实现正确性的手段,在正式发布的版本中,断言是不起作用的。在java中,启用断言需要增加一个编译选项,不过可以通过抛出异常来达到相同目的,使用异常比断言要危险,因为在程序的正式发布版本中会引起崩溃,不过有时候崩溃总比程序的诡异行为更好,例如:

// 表示集合的类public class Collection{  // 添加元素到集合中  public void addElement(Element e){};   // 获取指定位置的元素  public void getElement(int index){}; }// 表示只读集合的类public class ReadOnlyCollection extends Collection{  // 添加元素到集合中  public void addElement(Element e){    throw new UnsupportedOperationException("只读集合,不允许添加元素");  }    // 获取指定位置的元素  public void getElement(int index){};}

调用ReadOnlyColletion派生类必须遵守规矩:不能调用addElement,否则抛出异常干掉程序!

第七计:串联函数调用

当一个类的大部分函数被较为频繁地调用,并且包含连续性地调用,那么可以考虑为这个类中那些没有返回值的函数增加返回值,即返回对象本身,这样就可以串联函数调用,使用起来较为方便,举个例子:

// 表示用户的类public class User{  public void setName(String name);  public void setAge(int age);  public void setPhoneNumber(int phoneNumber);}

下面是不使用串联函数调用的情况:

User user = new User();user.setName("West_Link");user.setAge(3);user.setPhoneNumber(122333);

下面是使用串联函数调用的情况:

User user = new User().setName("West_Link").setAge(3).setPhoneNumber(123333);

只需要为那些函数增加一个User对象的返回值即可,如下:

public User setName(String name){   this.name = name;   return this;}

第八计:临时变量在用到时才声明

很多人喜欢在函数的开头把所有要用到的临时变量都声明了,我认为这种方式有以下几个缺点:

1、不利于代码的阅读,需要经常在变量的使用处和变量的声明处跳转,不方便。
2、容易造成资源的浪费,因为有些对象的初始化是比较耗费资源的,而函数可能在用到该对象之前返回。
3、不利于函数的拆分。
所以,我们应该尽可能降低临时变量的作用域,那样它能“捣乱“的范围就被缩至最小了。

第九计:保持类的公有函数粒度最小化

一个类的公有函数不应该过多,那样会使类变得臃肿、不易使用,我认为最佳状态是绝大部分公有函数不能被拆分,也就是说,不存在那些通过其他公有函数也能达到目的的函数,例如下面的类:

public class StringArray{  // 获取数组的大小  public int getSize();  // 判断数组是否为空  public boolean isEmpty();  // 将数据添加到数组的某个索引  public void add(String value,int index);  // 将数据添加到数组的末尾  public void addToLast(String value);  // 将数据添加到数组的起始位置  public void addToFirst(String value);}

StringArray其实只需要两个公有函数即可,即getSize和add,因为isEmpty可以通过getSize来达到相同的目的:getSize() == 0。类似地,addToLast、addToFirst也可以通过add来实现。不过,如果类的公有函数比较少,而且类似isEmpty类似的需求经常被用到,那么保留这些公有函数还是值得的。

第十计:将可访问性降至最低

面向对象中的封装性使得我们可以隐藏类或者接口的实现细节,所以,为了让程序更易维护、接口或者类的使用更加简单,我们应该尽可能降低成员变量或者成员函数的可访问性,辅助函数一定要声明为私有的,确保只将接口函数声明为公有的,如果该辅助函数可能会被子类用到,则可以声明为保护的。辅助类也应声明为私有的,对于成员变量则一定要声明为私有的,只提供必要的set或者get函数。总之,当我们增加新的类、成员变量、成员函数时,一定要合理地设置可访问性,暴露给外界的越少越好。

第十一计:合并条件减少嵌套

条件判定如果嵌套的太多会大大降低程序的可读性,很容易滋生Bug,例如:

if(a){  if(b || c){      if(d){          ...       }   }}

可以合并成:

if(a && (b || c) && d){     ...}

第十二计:循环中早用continue减少条件嵌套

除了通过合并条件来减少嵌套层次外,在for或者while循环中,可以用continue来减少条件嵌套,例如:

for(int i=0; i<100;i++){   if(a){      if(b){          if(c){               ...          }       }    }}

这段代码的嵌套深度是4,使用continue可以大大减少嵌套层次:

for(int i=0; i<100;i++){  if(!a)     continue;  if(!b)     continue;  if(!c)     continue;   ...}

第十三计:为集合类型的成员变量提供增删改查函数

对于集合类型的成员变量,直接提供一个函数将其暴露出去是不够妥当的,缺点包括以下几点:

1、添加集合元素时无法校验它的合法性,例如类型是否符合要求、该元素是否已存在等。
2、无法控制集合的容量,集合的容量可能是动态分配的。
所以,更好的做法是为集合类型的成员变量提供增删改查等函数,例如某个类中有名为userList的数组,则可以为它增加几个函数:

public void add(User user){  if(user == null) // 如果对象为不合法,则不添加     return;  if(userList == null) // 如果集合还未初始化,则新建    userList = new ArrayList();  if(isUserExisted(user)) // 如果该用户已经存在,则不添加    return;  // 将该用户添加到集合中  userList.add(user);}public void delete(User user){  if(userList == null)    return;  userList.remove(user);}

第十四计:避免一个临时变量充当多种角色

当在函数中声明一个临时变量的时候,其实已经设定了该变量的角色,这或多或少能从它的命名中看出来,例如下面变量的声明:

String userName = null;  

可以看出这个临时变量是存储用户名用的,有些人认为声明多个临时变量会浪费空间,所以在函数中会多次使用该变量,例如用这个变量存储了用户的密码:

userName = “*******”;

这种方式很容易引入Bug,而且降低了可理解性。因此,一个变量应该只充当一种角色。

第十五计:引入NULL Object来避免大量的对象合法性判断

当我们获得对象的引用后,在调用该对象的函数前一般都会检查它是否为NULL来避免程序崩溃,这样也会导致程序中出现大量类似下面的代码段:

EventRecorder recorder = EventRecorderFactory.getRecorderByType(0);if( recorder ==null ){  Log.error("Recorder对象为空");  lastErrorCode =0;}else{  recorder.record("记录点啥...");}

而NULL Object模式则可以避免这种情况,具体内容请参见:GoF著作中未提到的设计模式(3):Null Object

第十六计:函数命名有语法

大部分函数的命名尽量采用动词+名词的形式,并使其具有自注释性,例如:findUserById,从函数名中不仅能看出函数的功能,甚至连参数也能猜出来,另外,有些命名方式是有一定意义的,例如作为回调的函数一般以on开头,如:onUserPasswordChanged,说明该函数会在用户密码变化时被调用。对于返回布尔值的函数尽量采用疑问句式,如:isNameValid。

第十七计:去除只是内部状态不同的派生类

当某些派生类与父类相比只是状态不同时,那就应该考虑去掉这些派生类,把这些状态作为父类的成员变量,并且可以为原来派生类所表示的对象准备一些构造函数或者工厂方法,例如下面表示员工的类:

public abstract class Employee{  private int id;  private String name;  // 获取薪水  public abstract int getSalary();  // 获取是否有解雇员工的权利  public abstract boolean canFireOthers();}public class Programmer extends Employee{  public int getSalary(){    return 5000;  }  public boolean canFireOthers(){    return false;  }}public class Manager extends Employee{  public int getSalary(){    return 10000;  }  public boolean canFireOthers(){    return true;  }}

可以看出,Employee的派生类实质上只是状态与父类不同,应该将它们合并为一个类:

public class Employee{  private int id;  private String name;  private int salary;  private boolean canFireOthers;  public static Employee newProgrammer(String name){    return new Employee(name,5000,false);  }  public static Employee newManager(String name){    return new Employee(name,10000,true);  }  public Employee(String name,int salary,boolean   canFireOthers){    this.name = name;    this.salary = salary;    this.canFireOthers = canFireOthers;  }  // 获取薪水  public int getSalary(){    return salary;  }  // 获取是否有解雇员工的权利  public boolean canFireOthers(){    return canFireOthers;  }}

第十八计:少用标记变量

标记变量一般都是布尔类型的变量,主要用来在某类事件或者操作发生后做个标记,然后其他地方会用到这个标记,用完之后很可能会重置这个标记到初始状态。少量并恰当合理地使用标记变量可以达到很好的效果,能解决一些难题,不过,用的多了就会出乱子,尤其是一个类中有多个成员变量是标记变量,成员函数的实现不得不“看它们的眼色行事“了,它们所产生的标记值的组合会让实现者越来越头大,它们的同时存在增加了程序的复杂性。所以,当类中出现多个标记变量而隐约感觉到不对头时,应该果断干掉它们,然后认真分析处理逻辑是否存在问题,避免再次引入标记变量。

第十九计:避免类的臃肿

在我接触过的大部分项目中,总会有一个“大胖子“类特别惹眼,一般来说,这“大胖子“实际上是整个系统的核心类之一,之所以“胖“,主要原因是很多人都会把自己需要的函数加到这个类中,却没有人主动请缨来为它“减肥“。可以通过以下几种方式来为它“瘦身”:

1、按照某种特性(如功能、类型等)将这个类拆分成多个类。

2、合并冗余函数,保持函数粒度的最小化。

3、去除重复代码。

如果实在不能再“瘦”了,那就通过实现相应的接口,让它“看上去很瘦“,举个例子:

public class BigBoy{  public void foo1();  public void foo2();  public void foo3();  public void foo4();  public void foo5();  public void foo6();  public void foo7();  public void foo8();  public void foo9();  // 还有很多...}

这个类有很多函数,这让类的使用者很头疼,没办法,它不能再“瘦“了,不过,我们可以根据某种特性把这些函数抽象成多个接口,例如foo1、foo2、foo3可以抽象成一个接口:

public interface LittleBoy{  public void foo1();  public void foo2();  public void foo3();}

然后让BigBoy实现这个接口,并提供一个将BigBoy变成LitterBoy的函数:

public class BigBoy implements LittleBoy{  public LittleBoy asLittleBoy{)    return this;  }  public void foo1();  public void foo2();  public void foo3();  public void foo4();  public void foo5();  public void foo6();  public void foo7();  public void foo8();  public void foo9();  // 还有很多...}

这样,类的使用者得到将是“瘦版“的BigBoy,使用难度大大降低了,因此,对于需要使用该类所有函数中某个子集的用户,我们可以提供一个包含该函数子集的接口实现对象即可。

第二十计:保持代码风格的一致性

程序员或多或少都有各自的代码风格,当我们看和自己风格不同的人写的代码时都觉得有点别扭,甚至会影响读代码的流畅性,记得以前有个同事把for语句当if语句用,像下面这样,太个性了,看他的代码真有点头疼。

for(;value>10;){  ...}

整个程序保持一致的代码风格还是比较重要的,如果看着就像一个人写那就太到位了,所以,最好能在项目初期就统一程序的命名规范、通过逻辑处理规范、注释规范等。

第二十一计:成员变量要封装

大部分情况下,类中的成员变量都应该被声明为私有的,为那些需要被其他类访问的变量增加set或者get函数。如果成员变量声明为公有的,那么类就失去了对它们的控制权!封装成员变量有以下几个好处:

1、在为成员变量赋值时进行有效性校验或其他预处理操作。

2、在返回成员变量的值时进行二次包装,当该变量不可用或未初始化时返回默认值。

为成员变量分别增加set和get函数是一件挺繁琐的事情,很多人嫌麻烦,觉得公有成员变量更方便,不过从可维护性和可扩展性的角度看,添加set和get是值得的,Eclipse中可以自动生成set和get函数的,非常方便。

第二十二计:用自注释性变量代替复杂条件

嵌套层次较深的IF判断、没有注释的复杂条件大大增加了程序的逻辑复杂性,严重降低代码的可读性。对于子条件较多的条件判定,可以为各个子条件引入具有自注释性的临时变量来降低复杂性。例如下面是判断登录用户是否能进入网站的后台管理界面:

if(userName!=null && userName.equals(name) && (userState != INACTIVE || userState != DELETE) && userPassword!=null && userPassword.equals(password) && (userGroup == "Manager" || userGroup == "Root")){  ...}

下面是修改后的版本:

  boolean isUserNameValid = userName!=null && userName.equals(name);  boolean isUserActive = userState != INACTIVE || userState != DELETE;  boolean isUserPasswordCorrect = userPassword!=null && userPassword.equals(password);  boolean isUserHasAuth = userGroup == "Manager" || userGroup == "Root";  if(isUserNameValid && isUserActive && isUserPasswordCorrect && isUserHasAuth){     ...  }

第二种方式不仅使条件判断更具有可读性,还能重用子条件。

第二十三计:避免重复代码

重复代码是破坏程序可维护性的重量级选手之一,大量的重复代码会使代码量膨胀,修改重复的代码也很繁琐,改了一处后必须同时修改和它重复的代码,因此非常引入Bug,当有人修改了某处代码而忘记修改其他除重复的代码,那么Bug就出现了。所以,一旦要拷贝某段代码,请先考虑把这段代码通用化。

第二十四计:增加注释

记得在第一个公司工作的时候,公司很多程序员的代码注释率在40%左右,一般都是先写注释,然后紧接着写代码,因为注释也是一种文档。很多人觉得写注释浪费时间或者没有必要,所以他们的代码中没有绿色,或者只是星星点点,如果代码有一定的复杂性,那么其他人看这部分代码可能会比代码作者要费劲的多,注释就是帮助别人快速理解自己写的代码。

第二十五计:函数体最多不超过100行

记得以前看过一个函数有9000多行,很壮观啊,从那以后看到长函数时也不奇怪了,我认为过长函数的主要缺点是:

1、严重影响代码的阅读,使用到某个变量的地方可能间隔几百甚至上千行,如果if-else嵌套层次较多的话那就更噩梦了。

2、不利于代码的重用,短小而独立的逻辑处理单元更容易被重用,而过长的代码段则需要经过进一步分解才行。

我觉得函数最好不要超过100行,对于过长的函数要尽可能地进行分解,如果实在不能分解,那么就通过注释的方式增加该函数处理步骤的说明,例如:

复制代码
public void foo(){// 1、验证参数、内部状态的有效性...// 2、开始倾斜角度...// 2.1 计算角度1...// 2.2 计算角度2...// 3、输出计算说明书...}
复制代码

第二十六计:使用语言的修饰符确保变量的不可变性

当声明一个变量时,如果能十分确定该变量不会被修改或者不应该被修改,那最好把它声明为不可变的,如使用Java中的final、C++中的const修饰符,这样可以防止本该不变的变量被意外地修改。

第二十七计:对象状态共享

大量对象的同时存在会占用系统宝贵的内存,如果这些对象中某些状态是相同的,那么可以将这些状态提取出来让所有需要它的对象共享,这可以大大减少冗余对象,从而达到节省内存的目的,设计模式中的Flyweight模式可以解决这个问题。

第二十八计:用对象代替普通常量

由于普通常量本质上是一个简单的数字或者字符串,当我们错误地将某个类别的常量在另一个类别的常量的场景中使用时,就会产生问题,但是编译器并不会提示有错误,所以,这可能是一个不小的隐患,例如:

复制代码
// 表示用户状态的常量声明public static int USER_STATE_ACTIVE = 0;public static int USER_STATE_DELETE = 1;// 表示用户角色的常量声明public static int USER_ROLE_NORMAL = 2;public static int USER_ROLE_MANAGER = 3;// 下面用户是否被激活的判断if(userState == USER_ROLE_NORMAL){}
复制代码

这个判断本应该使用USER_STATE_ACTIVE和USER_STATE_DELETE两个常量之一,却意外地使用了其他常量,可能直到Bug产生后才能被发现。

可以使用对象常量来避免这种情况,例如:

复制代码
public class State{  private int state;  public State(int s){    state = s;  }}// 表示用户状态的常量声明public static State USER_STATE_ACTIVE = new State(0);public static State USER_STATE_DELETE = new State(1);public class Role{  private int role;  public Role(int r){    role = r;  }}// 表示用户角色的常量声明public static Role USER_ROLE_NORMAL = new Role(2);public static Role USER_ROLE_MANAGER = new Role(3);
复制代码

下面的判断是无法通过编译的,因为userState是State类型的。

if(userState == USER_ROLE_NORMAL){}

第二十九计:查询函数中尽量不要有修改操作

我们一般都是根据函数的名字来判断它的功能,“表里不一“的函数可能会引起一些问题,例如我们调了一个查询函数(获取类的成员变量值的函数):getName(),但是它内部却修改了其他成员变量的值,当查找Bug的原因时,很可能会忽略这个函数,从它的名字看,觉得它不会引起问题,到最后发现就是它捣的鬼,心里估计会骂这个函数的作者:他奶奶的,代码如其人,表里不一!

第三十计:尽量封装对象的创建过程

本文之前曾提到过要尽量为成员变量增加set和get函数,主要目的是为了掌握成员变量的控制权,对象的创建过程也是如此,如果提供者掌握了对象创建过程的控制权,那么就可以屏蔽具体的实现类,并且任意修改对象的创建过程。

原创粉丝点击