熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

敏捷開發技巧-消除代碼異味

2022-06-13   來源: Java核心技術 

  摘要:
本文通過簡單通俗的例子 告訴我們如何判斷代碼的穩定性和代碼中的異類 並且如何重構此類代碼

異味這個詞可能有點抽象我們先看一下下面的例子

這是一個CAD系統 現在它已經可以畫三種形狀了:線條長方形跟圓先認真的看一下下面的代碼:
class Shape {                                                                                   
       final static int TYPELINE = ;                                                               
       final static int TYPERECTANGLE = ;                                                          
       final static int TYPECIRCLE = ;                                                             
       int shapeType;                                                                               
       //線條的開始點
     //長方形左下角的點
     //圓心
     Point p;                                                                                    
       //線條的結束點
     //長方形的右上角的點
     //如果是圓的話這個屬性不用
     Point p;                                                                                    
       int radius;                                                                                  
    }                                                                                               
class CADApp {                                                                                  
       void drawShapes(Graphics graphics Shape shapes[]) {                                         
           for (int i = ; i < shapeslength; i++) {                                                
               switch (shapes[i]getType()) {                                                       
                  case ShapeTYPELINE:                                                              
                      graphicsdrawLine(shapes[i]getP() shapes[i]getP());                      
                      break;                                                                        
                  case ShapeTYPERECTANGLE:                                                         
                      //畫四條邊
               graphicsdrawLine();                                                       
                      graphicsdrawLine();                                                       
                      graphicsdrawLine();                                                       
                      graphicsdrawLine();                                                       
                      break;                                                                        
                  case ShapeTYPECIRCLE:                                                            
                      graphicsdrawCircle(shapes[i]getP() shapes[i]getRadius());                
                      break;                                                                        
               }                                                                                    
           }                                                                                        
       }                                                                                            
    } 
                                                                                              


    代碼都是一直在改變的而這也是上面的代碼會碰到的一個問題

    現在我們有一個問題: 如果我們需要支持更多的形狀(比如三角形) 那麼肯定要改動Shape這個類 CADApp裡面的drawShapes這個方法也要改
改為如下的樣子:
    
class Shape {      
       final static int TYPELINE = ;
       final static int TYPERECTANGLE = ;
       final static int TYPECIRCLE = ;
       final static int TYPETRIANGLE = ;
       int shapeType;  
       Point p;       
       Point p;       
       //三角形的第三個點
       Point p;       
       int radius;     
    }                  
class CADApp {     
     void drawShapes(Graphics graphics Shape shapes[]) {
         for (int i = ; i < shapeslength; i++) {
             switch (shapes[i]getType()) {
                case ShapeTYPELINE:
                    graphicsdrawLine(shapes[i]getP() shapes[i]getP());
                    break;
                case ShapeTYPERECTANGLE:
                      //畫四條邊
                    graphicsdrawLine();
                    graphicsdrawLine();
                    graphicsdrawLine();
                    graphicsdrawLine();
                    break;
                case ShapeTYPECIRCLE:
                    graphicsdrawCircle(shapes[i]getP() shapes[i]getRadius());
                    break;
                case ShapeTYPETRIANGLE:
                    graphicsdrawLine(shapes[i]getP() shapes[i]getP());
                    graphicsdrawLine(shapes[i]getP() shapes[i]getP());
                    graphicsdrawLine(shapes[i]getP() shapes[i]getP());
                    break;
               }       
           }           
       }               
  }
                  

    如果以後要支持更多的形狀這些類又要改動……這可不是什麼好事情!
   理想情況下我們希望當一個類一個方法或其他的代碼設計完以後就不用再做修改了它們應該穩定到不用修改就可以重用 
    現在的情況恰好相反!
    每當我們增加新的形狀都得修改Shape這個類跟CADApp裡面的drawShapes方法

    怎麼讓代碼穩定(也就是無需修改)?這個問題是個好問題!不過老規矩先不說我們以行動回答
    我們先看看另外一個方法 當給你一段代碼你怎麼知道它是穩定的?


  怎麼判斷代碼的穩定性?

    要判斷代碼的穩定性我們可能會這樣來判定先假設一些具體的情況或者需求變動了然後來看一看要滿足這些新的需求代碼是否需要被修改? 
    可惜這也是一件很麻煩的事因為有那麼多的可能性!我們怎麼知道哪個可能性要考慮哪些不用考慮?

    有個更簡單的方法 如果發現說我們已經第三次修改這些代碼了那我們就認定這些代碼是不穩定的這個方法很懶惰而且被動!我們被傷到了才開始處理狀況不過至少這種方法還是一個很有效的方法

    此外還有一個簡單而且主動的方法如果這段代碼是不穩定或者有一些潛在問題的那麼代碼往往會包含一些明顯的痕跡正如食物要腐壞之前經常會發出一些異味一樣(當然食物如果有異味了再怎麼處理我們都不想吃了但是代碼可不行我們管這些痕跡叫做代碼異味正如並不是所有的食物有異味都不能吃了但大多數情況下確實是不能吃了並不是所有的代碼異味都是壞事但大多數情況下它們確實是壞事情!因此當我們感覺出有代碼異味時我們必須小心謹慎的檢查了

    現在我們來看看上面例子中的代碼異味吧

    示例代碼中的代碼異味

    第一種異味代碼用了類別代碼(type code)
  
class Shape {                                                                                   
     final int TYPELINE = ;                                                                      
     final int TYPERECTANGLE = ;                                                                 
     final int TYPECIRCLE = ;                                                                    
     int shapeType;                                                                               
                                                                                               
  }    
                                                                                           

    這樣的異味是一種嚴肅的警告我們的代碼可能有許多問題      

    第二種異味Shape這個類有很多屬性有時候是不用的例如radius這個屬性只有在這個Shape是個圓的時候才用到
    class Shape {                                                                                   
                                                                                                 
       Point p;                                                                                    
       Point p;                                                                                    
       int radius; //有時候不用                                                             
    }   
                                                                                            

    第三種異味我們想給pp取個好一點的變量名都做不到因為不同的情況下它們有不同的含義
    class Shape {  
                
       Point p; //要取作起始點左下點還是圓心
     Point p;   
    }        
      

    第四種異味drawShapes這個方法裡面有個switch表達式當我們用到switch(或者一大串的ifthenelseif)時小心了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了而要改用ifthenelse 
    class CADApp { 
       void drawShapes(Graphics graphics Shape shapes[]) {
           for (int i = ; i < shapeslength; i++) {
               if (shapes[i] instanceof Line) {
                  Line line = (Line)shapes[i];
                  graphicsdrawLine(linegetStartPoint()linegetEndPoint());
               } else if (shapes[i] instanceof Rectangle) {
                  Rectangle rect = (Rectangle)shapes[i];
                  graphicsdrawLine();
                  graphicsdrawLine();
                  graphicsdrawLine();
                  graphicsdrawLine();
               } else if (shapes[i] instanceof Circle) {
                  Circle circle = (Circle)shapes[i];
                  graphicsdrawCircle(circlegetCenter() circlegetRadius());                      
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }     
                                                                                          

    因為沒有類別代碼了現在每個類(ShapeLineRectangleCircle)裡面的所有屬性就不會有時用得到有時用不到了現在我們也可以給它們取一些好聽點的名字了(比如在Line裡面p這個屬性可以改名為startPoint了)現在四種異味只剩一種了那就是在drawShapes裡面還是有一大串ifthenelseif我們下一步就是要去掉這長長的一串
    

    消除代碼異味如何去掉一大串ifthenelseif(或者switch)        

    經常地為了去掉ifthenelseif或者switch我們需要先保證在每個條件分支下的要寫的代碼是一樣的在drawShapes這個方法裡面我們先以一個較抽象的方法(偽碼)來寫吧
    
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics Shape shapes[]) {                                         
           for (int i = ; i < shapeslength; 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 = ; i < shapeslength; 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 = ; i < shapeslength; i++) {
               畫出形狀;
           }    
       }        
    }       
    

    最後畫出形狀這個偽碼寫成代碼吧:
    class CADApp {
       void drawShapes(Graphics graphics Shape shapes[]) {
           for (int i = ; i < shapeslength; 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) {
           graphicsdrawLine(getStartPoint() getEndPoint());
       }        
    }           
    class Rectangle extends Shape {
       Point lowerLeftCorner; 
       Point upperRightCorner; 
       void draw(Graphics graphics) {
           graphicsdrawLine();
           graphicsdrawLine();
           graphicsdrawLine();
           graphicsdrawLine();
       }        
    }           
    class Circle extends Shape {
       Point center; 
       int radius;
       void draw(Graphics graphics) {
           graphicsdrawCircle(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) {                                                               
           graphicsdrawLine(getStartPoint() getEndPoint());                                       
       }                                                                                            
    }                                                                                               
    class Rectangle implements Shape {                                                              
       Point lowerLeftCorner;                                                                       
       Point upperRightCorner;                                                                      
       void draw(Graphics graphics) {                                                               
           graphicsdrawLine();                                                                  
           graphicsdrawLine();                                                                  
           graphicsdrawLine();                                                                  
           graphicsdrawLine();                                                                  
       }                                                                                            
    }                                                                                               
    class Circle implements Shape {                                                                 
       Point center;                                                                                
       int radius;                                                                                  
       void draw(Graphics graphics) {                                                               
           graphicsdrawCircle(getCenter() getRadius());                                           
       }                                                                                            
    }                                                                                               
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics Shape shapes[]) {                                         
           for (int i = ; i < shapeslength; i++) {                                                
               shapes[i]draw(graphics);                                                            
           }                                                                                        
       }                                                                                            
    } 
                                                                                              

    如果我們想要支持更多的圖形(比如三角形)上面沒有一個類需要修改我們只需要創建一個新的類Triangle就行了



   另一個例子

    讓我們來看一下另外一個例子在當前的系統中有三種用戶常規用戶管理員和游客
    常規用戶必須每隔天修改一次密碼(更頻繁也行)管理員必須每天修改一次密碼游客就不需要修改了
    常規用戶跟管理員可以打印報表
    先看一下當前的代碼
    class UserAccount {
       final static int USERTYPE_NORMAL = ;
       final static int USERTYPE_ADMIN = ;
       final static int USERTYPE_GUEST = ;
       int userType;
       String id;  
       String name;
       String password;
       Date dateOfLastPasswdChange;
       public boolean checkPassword(String password) {
                
       }           
    }              
    class InventoryApp {
       void login(UserAccount userLoggingIn String password) {
           if (userLoggingIncheckPassword(password)) {
               GregorianCalendar today = new GregorianCalendar();
               GregorianCalendar expiryDate = getAccountExpiryDate(userLoggingIn);
               if (todayafter(expiryDate)) {
                  //提示用戶修改密碼
                  
               }   
           }       
       }           
       GregorianCalendar getAccountExpiryDate(UserAccount account) {
           int passwordMaxAgeInDays = getPasswordMaxAgeInDays(account);
           GregorianCalendar expiryDate = new GregorianCalendar();
           expiryDatesetTime(accountdateOfLastPasswdChange);
           expiryDateadd(CalendarDAY_OF_MONTH passwordMaxAgeInDays);
           return expiryDate;
       }           
       int getPasswordMaxAgeInDays(UserAccount account) {
           switch (accountgetType()) {
               case UserAccountUSERTYPE_NORMAL:
                  return ;
               case UserAccountUSERTYPE_ADMIN:
                  return ;
               case UserAccountUSERTYPE_GUEST:
                  return IntegerMAX_VALUE;
           }       
       }           
       void printReport(UserAccount currentUser) {
           boolean canPrint;
           switch (currentUsergetType()) {
               case UserAccountUSERTYPE_NORMAL:
                  canPrint = true;
                  break;
               case UserAccountUSERTYPE_ADMIN:
                  canPrint = true;
                  break;                                                                            
               case UserAccountUSERTYPE_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 ;                                                                               
       }                                                                                            
       boolean canPrintReport() {                                                                   
           return true;                                                                             
       }                                                                                            
    }                                                                                               
    class AdminUserAccount extends UserAccount {                                                    
       int getPasswordMaxAgeInDays() {                                                              
           return ;                                                                               
       }                                                                                            
       boolean canPrintReport() {                                                                   
           return true;                                                                             
       }                                                                                            
    }                                                                                               
    class GuestUserAccount extends UserAccount {                                                    
       int getPasswordMaxAgeInDays() {                                                              
           return IntegerMAX_VALUE;                                                                
       }                                                                                            
       boolean canPrintReport() {                                                                   
           return false;                                                                            
       }                                                                                            
    }
                                                                                               

    但問題是三種子類的行為(裡面的代碼)都差不多一樣getPasswordMaxAgeInDays這個方法就一個數值不同(或者IntegerMAX_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) {
           thispasswordMaxAgeInDays = passwordMaxAgeInDays;
           thisallowedToPrintReport = allowedToPrintReport;
       }           
       int getPasswordMaxAgeInDays() {
           return passwordMaxAgeInDays;
       }           
       boolean canPrintReport() {
           return allowedToPrintReport;
       }           
       static UserType normalUserType = new UserType( true);
       static UserType adminUserType = new UserType( true);
       static UserType guestUserType = new UserType(IntegerMAX_VALUE false);
    }              
    class InventoryApp {
       void login(UserAccount userLoggingIn String password) {
           if (userLoggingIncheckPassword(password)) {
               GregorianCalendar today = new GregorianCalendar();
               GregorianCalendar expiryDate = getAccountExpiryDate(userLoggingIn);
               if (todayafter(expiryDate)) {
                  //提示用戶修改密碼
                  
               }   
           }       
       }           
       GregorianCalendar getAccountExpiryDate(UserAccount account) {
           int passwordMaxAgeInDays = getPasswordMaxAgeInDays(account);
           GregorianCalendar expiryDate = new GregorianCalendar();
           expiryDatesetTime(accountdateOfLastPasswdChange);
           expiryDateadd(CalendarDAY_OF_MONTH passwordMaxAgeInDays);
           return expiryDate;
       }           
       int getPasswordMaxAgeInDays(UserAccount account) {
           return accountgetType()getPasswordMaxAgeInDays();
       }           
       void printReport(UserAccount currentUser) {
           boolean canPrint;
           canPrint = currentUsergetType()canPrintReport();
           if (!canPrint) {
               throw new SecurityException(You have no right);
           }       
           //打印報表
       }           
    } 
                                                                                              

    注意到了吧用一個對象代替類別同樣可以移除switch或者ifthenelseif       

總結一下類別代碼的移除

    要移動一些類別代碼和switch表達式有兩種方法                            
    用基於同一父類的不同子類來代替不同的類別                                        
    用一個類的不同對象來代替不同的類別
    當不同的類別具有比較多不同的行為時用第一種方法當這些類別的行為非常相似或者只是差別在一些值上面的時候用第二個方法

     普遍的代碼異味
    類別代碼和switch表達式是比較普遍的代碼異味此外還有其他的代碼異味也很普遍
    下面是大概的異味列表
    代碼重復
    太多的注釋
    類別代碼(type code)

   switch或者一大串ifthenelseif
    想給一個變量方法或者類名取個好名字時也怎麼也取不好    
    用類似XXXUtil XXXManager XXXController 和其他的一些命名 
    在變量方法或類名中使用這些單詞AndOr等等   
    一些實例中的變量有時有用有時沒用
    一個方法的代碼太多或者說方法太長
    一個類的代碼太多或者說類太長
    一個方法有太多參數
    兩個類都引用了彼此(依賴於彼此)


From:http://tw.wingwit.com/Article/program/Java/hx/201311/27159.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.