网站首页
JSP空间
动态资讯
开源项目
技术文档
资源下载
J2EE资源
客户论坛
在线支付
 
  技术文档>>JAVA>>新手入门>>基础入门>查看文档  
  敏捷开发技巧-消除代码异味     
  文章作者:未知  文章来源:水木森林  
  查看:131次  录入:管理员--2007-11-17  
 

摘要:
本文通过简单通俗的例子, 告诉我们如何判断代码的稳定性和代码中的异类, 并且如何重构此类代码.

异味这个词,可能有点抽象,我们先看一下下面的例子

这是一个cad系统. 现在,它已经可以画三种形状了:线条,长方形,跟圆.先认真的看一下下面的代码:
class shape {                                                                                   
       final static int typeline = 0;                                                               
       final static int typerectangle = 1;                                                          
       final static int typecircle = 2;                                                             
       int shapetype;                                                                               
       //线条的开始点
     //长方形左下角的点
     //圆心
     point p1;                                                                                    
       //线条的结束点
     //长方形的右上角的点
     //如果是圆的话,这个属性不用
     point p2;                                                                                    
       int radius;                                                                                  
    }                                                                                               
class cadapp {                                                                                  
       void drawshapes(graphics graphics, shape shapes[]) {                                         
           for (int i = 0; i < shapes.length; i++) {                                                
               switch (shapes[i].gettype()) {                                                       
                  case shape.typeline:                                                              
                      graphics.drawline(shapes[i].getp1(), shapes[i].getp2());                      
                      break;                                                                        
                  case shape.typerectangle:                                                         
                      //画四条边
               graphics.drawline(...);                                                       
                      graphics.drawline(...);                                                       
                      graphics.drawline(...);                                                       
                      graphics.drawline(...);                                                       
                      break;                                                                        
                  case shape.typecircle:                                                            
                      graphics.drawcircle(shapes[i].getp1(), shapes[i].getradius());                
                      break;                                                                        
               }                                                                                    
           }                                                                                        
       }                                                                                            
    } 
                                                                                              

    代码都是一直在改变的,而这也是上面的代码会碰到的一个问题.

    现在我们有一个问题: 如果我们需要支持更多的形状(比如三角形), 那么肯定要改动shape这个类, cadapp里面的drawshapes这个方法也要改.
好,改为如下的样子:
    
class shape {      
       final static int typeline = 0;
       final static int typerectangle = 1;
       final static int typecircle = 2;
       final static int typetriangle = 3;
       int shapetype;  
       point p1;       
       point p2;       
       //三角形的第三个点.
       point p3;       
       int radius;     
    }                  
class cadapp {     
     void drawshapes(graphics graphics, shape shapes[]) {
         for (int i = 0; i < shapes.length; i++) {
             switch (shapes[i].gettype()) {
                case shape.typeline:
                    graphics.drawline(shapes[i].getp1(), shapes[i].getp2());
                    break;
                case shape.typerectangle:
                      //画四条边.
                    graphics.drawline(...);
                    graphics.drawline(...);
                    graphics.drawline(...);
                    graphics.drawline(...);
                    break;
                case shape.typecircle:
                    graphics.drawcircle(shapes[i].getp1(), shapes[i].getradius());
                    break;
                case shape.typetriangle:
                    graphics.drawline(shapes[i].getp1(), shapes[i].getp2());
                    graphics.drawline(shapes[i].getp2(), shapes[i].getp3());
                    graphics.drawline(shapes[i].getp3(), shapes[i].getp1());
                    break;
               }       
           }           
       }               
  }
                  

    如果以后要支持更多的形状,这些类又要改动……,这可不是什么好事情!
   理想情况下,我们希望当一个类,一个方法或其他的代码设计完以后,就不用再做修改了。它们应该稳定到不用修改就可以重用。 
    现在的情况恰好相反!
    每当我们增加新的形状,都得修改shape这个类,跟cadapp里面的drawshapes方法。

    怎么让代码稳定(也就是无需修改)?这个问题是个好问题!不过老规矩,先不说,我们以行动回答。
    我们先看看另外一个方法: 当给你一段代码,你怎么知道它是稳定的?

 

   怎么判断代码的稳定性?

    要判断代码的稳定性,我们可能会这样来判定:先假设一些具体的情况或者需求变动了,然后来看一看,要满足这些新的需求,代码是否需要被修改? 
    可惜,这也是一件很麻烦的事,因为有那么多的可能性!我们怎么知道哪个可能性要考虑,哪些不用考虑?

    有个更简单的方法, 如果发现说,我们已经第三次修改这些代码了,那我们就认定这些代码是不稳定的。这个方法很“懒惰”,而且“被动”!我们被伤到了,才开始处理状况。不过至少这种方法还是一个很有效的方法。

    此外,还有一个简单,而且“主动”的方法:如果这段代码是不稳定或者有一些潜在问题的,那么代码往往会包含一些明显的痕迹。正如食物要腐坏之前,经常会发出一些异味一样(当然,食物如果有异味了,再怎么处理我们都不想吃了。但是代码可不行。)。我们管这些痕迹叫做“代码异味”。正如并不是所有的食物有异味都不能吃了,但大多数情况下,确实是不能吃了。并不是所有的代码异味都是坏事,但大多数情况下,它们确实是坏事情!因此,当我们感觉出有代码异味时,我们必须小心谨慎的检查了。

    现在,我们来看看上面例子中的代码异味吧。

    示例代码中的代码异味:

    第一种异味:代码用了类别代码(type code)。
  
class shape {                                                                                   
     final int typeline = 0;                                                                      
     final int typerectangle = 1;                                                                 
     final int typecircle = 2;                                                                    
     int shapetype;                                                                               
     ...                                                                                          
  }    
                                                                                           

    这样的异味,是一种严肃的警告:我们的代码可能有许多问题。      

    第二种异味:shape这个类有很多属性有时候是不用的。例如,radius这个属性只有在这个shape是个圆的时候才用到:
    class shape {                                                                                   
       ...                                                                                          
       point p1;                                                                                    
       point p2;                                                                                    
       int radius; //有时候不用                                                             
    }   
                                                                                            

    第三种异味:我们想给p1,p2取个好一点的变量名都做不到,因为不同的情况下,它们有不同的含义:
    class shape {  
       ...         
       point p1; //要取作“起始点”,“左下点”,还是“圆心”?
     point p2;   
    }        
      

    第四种异味:drawshapes这个方法里面,有个switch表达式。当我们用到switch(或者一大串的if-then-else-if)时,小心了。switch表达式经常是跟类别代码(type code)同时出现的。
    现在,让我们将这个示例中的代码异味消除吧。

    消除代码异味:怎么去掉类别代码(type code)

    大多数情况下,要想去掉一个类别代码,我们会为每一种类别建立一个子类,比如:
    (当然,并不是每次要去掉一个类别代码都要增加一个新类,我们下面的另一个例子里面会讲另一种解决方法)
    class shape {  
    }              
    class line extends shape {
       point startpoint; 
       point endpoint; 
    }              
    class rectangle extends shape {
       point lowerleftcorner; 
       point upperrightcorner; 
    }              
    class circle extends shape {
       point center; 
       int radius; 
    }              

    因为现在没有类别代码了,drawshapes这个方法里面,就要用instanceof来判断对象是哪一种形状了。因此,我们不能用switch了,而要改用if-then-else: 
    class cadapp { 
       void drawshapes(graphics graphics, shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               if (shapes[i] instanceof line) {
                  line line = (line)shapes[i];
                  graphics.drawline(line.getstartpoint(),line.getendpoint());
               } else if (shapes[i] instanceof rectangle) {
                  rectangle rect = (rectangle)shapes[i];
                  graphics.drawline(...);
                  graphics.drawline(...);
                  graphics.drawline(...);
                  graphics.drawline(...);
               } else if (shapes[i] instanceof circle) {
                  circle circle = (circle)shapes[i];
                  graphics.drawcircle(circle.getcenter(), circle.getradius());                      
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }     
                                                                                          

    因为没有类别代码了,现在每个类(shape,line,rectangle,circle)里面的所有属性就不会有时用得到有时用不到了。现在我们也可以给它们取一些好听点的名字了(比如在line里面,p1这个属性可以改名为startpoint了)。现在四种异味只剩一种了,那就是,在drawshapes里面还是有一大串if-then-else-if。我们下一步,就是要去掉这长长的一串。
    

    消除代码异味:如何去掉一大串if-then-else-if(或者switch)        

    经常地,为了去掉if-then-else-if或者switch,我们需要先保证在每个条件分支下的要写的代码是一样的。在drawshapes这个方法里面,我们先以一个较抽象的方法(伪码)来写吧:
    
    class cadapp {                                                                                  
       void drawshapes(graphics graphics, shape shapes[]) {                                         
           for (int i = 0; i < shapes.length; i++) {                                                
               if (shapes[i] instanceof line) {                                                     
                  画线条;                                                                    
               } else if (shapes[i] instanceof rectangle) {                                         
                  画长方形;                                                               
               } else if (shapes[i] instanceof circle) {                                            
                  画圆;                                                                  
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }     
                                                                                          

    条件下的代码还是不怎么一样,不如再抽象一点:
    class cadapp {                                                                                  
       void drawshapes(graphics graphics, shape shapes[]) {                                         
           for (int i = 0; i < shapes.length; i++) {                                                
               if (shapes[i] instanceof line) {                                                     
                  画出形状;                                                                   
               } else if (shapes[i] instanceof rectangle) {                                         
                  画出形状;                                                                   
               } else if (shapes[i] instanceof circle) {                                            
                  画出形状;                                                                   
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }     
                                                                                          

    好,现在三个分支下的代码都一样了。我们也就不需要条件分支了:
    
    class cadapp {                                                                                  
       void drawshapes(graphics graphics, shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               画出形状;
           }    
       }        
    }       
    

    最后,将“画出形状”这个伪码写成代码吧:
    class cadapp {
       void drawshapes(graphics graphics, shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               shapes[i].draw(graphics);
           }    
       }        
    }        
   
    
 

   当然,我们需要在每种shape的类里面提供draw这个方法:
    abstract class shape {
       abstract void draw(graphics graphics);
    }           
    class line extends shape {
       point startpoint; 
       point endpoint; 
       void draw(graphics graphics) {
           graphics.drawline(getstartpoint(), getendpoint());
       }        
    }           
    class rectangle extends shape {
       point lowerleftcorner; 
       point upperrightcorner; 
       void draw(graphics graphics) {
           graphics.drawline(...);
           graphics.drawline(...);
           graphics.drawline(...);
           graphics.drawline(...);
       }        
    }           
    class circle extends shape {
       point center; 
       int radius;
       void draw(graphics graphics) {
           graphics.drawcircle(getcenter(), getradius());
       }        
    }
           

    将抽象类变成接口

    现在,看一下shape这个类,它本身没有实际的方法。所以,它更应该是一个接口:

    interface shape {                                                                               
       void draw(graphics graphics);                                                                
    }                                                                                               
    class line implements shape {                                                                   
       ...                                                                                          
    }                                                                                               
    class rectangle implements shape {                                                              
       ...                                                                                          
    }                                                                                               
    class circle implements shape {                                                                 
       ...                                                                                          
    }    
                                                                                           

    改进后的代码

    改进后的代码就像下面这样:
    
    interface shape {                                                                               
       void draw(graphics graphics);                                                                
    }                                                                                               
    class line implements shape {                                                                   
       point startpoint;                                                                            
       point endpoint;                                                                              
       void draw(graphics graphics) {                                                               
           graphics.drawline(getstartpoint(), getendpoint());                                       
       }                                                                                            
    }                                                                                               
    class rectangle implements shape {                                                              
       point lowerleftcorner;                                                                       
       point upperrightcorner;                                                                      
       void draw(graphics graphics) {                                                               
           graphics.drawline(...);                                                                  
           graphics.drawline(...);                                                                  
           graphics.drawline(...);                                                                  
           graphics.drawline(...);                                                                  
       }                                                                                            
    }                                                                                               
    class circle implements shape {                                                                 
       point center;                                                                                
       int radius;                                                                                  
       void draw(graphics graphics) {                                                               
           graphics.drawcircle(getcenter(), getradius());                                           
       }                                                                                            
    }                                                                                               
    class cadapp {                                                                                  
       void drawshapes(graphics graphics, shape shapes[]) {                                         
           for (int i = 0; i < shapes.length; i++) {                                                
               shapes[i].draw(graphics);                                                            
           }                                                                                        
       }                                                                                            
    } 
                                                                                              

    如果我们想要支持更多的图形(比如:三角形),上面没有一个类需要修改。我们只需要创建一个新的类triangle就行了。

   另一个例子

    让我们来看一下另外一个例子。在当前的系统中,有三种用户:常规用户,管理员和游客。
    常规用户必须每隔90天修改一次密码(更频繁也行)。管理员必须每30天修改一次密码。游客就不需要修改了。
    常规用户跟管理员可以打印报表。
    先看一下当前的代码:
    class useraccount {
       final static int usertype_normal = 0;
       final static int usertype_admin = 1;
       final static int usertype_guest = 2;
       int usertype;
       string id;  
       string name;
       string password;
       date dateoflastpasswdchange;
       public boolean checkpassword(string password) {
           ...     
       }           
    }              
    class inventoryapp {
       void login(useraccount userloggingin, string password) {
           if (userloggingin.checkpassword(password)) {
               gregoriancalendar today = new gregoriancalendar();
               gregoriancalendar expirydate = getaccountexpirydate(userloggingin);
               if (today.after(expirydate)) {
                  //提示用户修改密码
                  ...
               }   
           }       
       }           
       gregoriancalendar getaccountexpirydate(useraccount account) {
           int passwordmaxageindays = getpasswordmaxageindays(account);
           gregoriancalendar expirydate = new gregoriancalendar();
           expirydate.settime(account.dateoflastpasswdchange);
           expirydate.add(calendar.day_of_month, passwordmaxageindays);
           return expirydate;
       }           
       int getpasswordmaxageindays(useraccount account) {
           switch (account.gettype()) {
               case useraccount.usertype_normal:
                  return 90;
               case useraccount.usertype_admin:
                  return 30;
               case useraccount.usertype_guest:
                  return integer.max_value;
           }       
       }           
       void printreport(useraccount currentuser) {
           boolean canprint;
           switch (currentuser.gettype()) {
               case useraccount.usertype_normal:
                  canprint = true;
                  break;
               case useraccount.usertype_admin:
                  canprint = true;
                  break;                                                                            
               case useraccount.usertype_guest:                                                     
                  canprint = false;                                                                 
           }                                                                                        
           if (!canprint) {                                                                         
               throw new securityexception("you have no right");                                    
           }                                                                                        
           //打印报表
       }                                                                                            
    } 
                                                                                              

 

    用一个对象代替一种类别(注意,之前是一个类代替一种类别)

    根据之前讲的解决方法,要去掉类别代码,我们只需要为每种类别创建一个子类,比如:
    
    abstract class useraccount {                                                                    
       string id;                                                                                   
       string name;                                                                                 
       string password;                                                                             
       date dateoflastpasswdchange;                                                                 
       abstract int getpasswordmaxageindays();                                                      
       abstract boolean canprintreport();                                                           
    }                                                                                               
    class normaluseraccount extends useraccount {                                                   
       int getpasswordmaxageindays() {                                                              
           return 90;                                                                               
       }                                                                                            
       boolean canprintreport() {                                                                   
           return true;                                                                             
       }                                                                                            
    }                                                                                               
    class adminuseraccount extends useraccount {                                                    
       int getpasswordmaxageindays() {                                                              
           return 30;                                                                               
       }                                                                                            
       boolean canprintreport() {                                                                   
           return true;                                                                             
       }                                                                                            
    }                                                                                               
    class guestuseraccount extends useraccount {                                                    
       int getpasswordmaxageindays() {                                                              
           return integer.max_value;                                                                
       }                                                                                            
       boolean canprintreport() {                                                                   
           return false;                                                                            
       }                                                                                            
    }
                                                                                               

    但问题是,三种子类的行为(里面的代码)都差不多一样,getpasswordmaxageindays这个方法就一个数值不同(30,90或者integer.max_value)。canprintreport这个方法也不同在一个数值(true或false)。这三种用户类型只需要用三个对象代替就行了,无须特地新建三个子类了:
    class useraccount {
       usertype usertype;
       string id;  
       string name;
       string password;
       date dateoflastpasswdchange;
       usertype gettype() {
           return usertype;
       }           
    }              
    class usertype {
       int passwordmaxageindays;
       boolean allowedtoprintreport;
       usertype(int passwordmaxageindays, boolean allowedtoprintreport) {
           this.passwordmaxageindays = passwordmaxageindays;
           this.allowedtoprintreport = allowedtoprintreport;
       }           
       int getpasswordmaxageindays() {
           return passwordmaxageindays;
       }           
       boolean canprintreport() {
           return allowedtoprintreport;
       }           
       static usertype normalusertype = new usertype(90, true);
       static usertype adminusertype = new usertype(30, true);
       static usertype guestusertype = new usertype(integer.max_value, false);
    }              
    class inventoryapp {
       void login(useraccount userloggingin, string password) {
           if (userloggingin.checkpassword(password)) {
               gregoriancalendar today = new gregoriancalendar();
               gregoriancalendar expirydate = getaccountexpirydate(userloggingin);
               if (today.after(expirydate)) {
                  //提示用户修改密码
                  ...
               }   
           }       
       }           
       gregoriancalendar getaccountexpirydate(useraccount account) {
           int passwordmaxageindays = getpasswordmaxageindays(account);
           gregoriancalendar expirydate = new gregoriancalendar();
           expirydate.settime(account.dateoflastpasswdchange);
           expirydate.add(calendar.day_of_month, passwordmaxageindays);
           return expirydate;
       }           
       int getpasswordmaxageindays(useraccount account) {
           return account.gettype().getpasswordmaxageindays();
       }           
       void printreport(useraccount currentuser) {
           boolean canprint;
           canprint = currentuser.gettype().canprintreport();
           if (!canprint) {
               throw new securityexception("you have no right");
           }       
           //打印报表.
       }           
    } 
                                                                                              

    注意到了吧,用一个对象代替类别,同样可以移除switch或者if-then-else-if。       

总结一下类别代码的移除

    要移动一些类别代码和switch表达式,有两种方法:                            
    1.用基于同一父类的不同子类来代替不同的类别。                                        
    2.用一个类的不同对象来代替不同的类别。
    当不同的类别具有比较多不同的行为时,用第一种方法。当这些类别的行为非常相似,或者只是差别在一些值上面的时候,用第二个方法。

     普遍的代码异味
    类别代码和switch表达式是比较普遍的代码异味。此外,还有其他的代码异味也很普遍。
    下面是大概的异味列表:
    代码重复
    太多的注释
    类别代码(type code)

   switch或者一大串if-then-else-if
    想给一个变量,方法或者类名取个好名字时,也怎么也取不好    
    用类似xxxutil, xxxmanager, xxxcontroller 和其他的一些命名 
    在变量,方法或类名中使用这些单词“and”,“or”等等   
    一些实例中的变量有时有用,有时没用
    一个方法的代码太多,或者说方法太长
    一个类的代码太多,或者说类太长
    一个方法有太多参数
    两个类都引用了彼此(依赖于彼此)

 
 
上一篇: 关于j2se的一些知识和技巧    下一篇: 如何制作最小的rcp程序压缩包
  相关文档
一个jdbc-odbc桥的字符集参数的例子 11-17
为java性能而设计(1) 11-17
开发狂想曲:如何在开源的java下生存? 11-16
整合se j2me sdk与eclipse开发环境 11-17
错误的堆大小产生的 11-17
unija技术体系管窥 11-17
让java动起来的脚本语言 11-17
解决中文问题的几个常用的函数(2) 11-17
在spring中使用jdo 11-17
eclipse插件开发之findbugs插件 11-16