高質量軟件開發人員的五大習慣(好)

 那些在團隊環境下有能力開發和維護高質量軟件的開發人員在今天的技術驅動的經濟裏有着巨大的需求。工作在團隊環境下的開發人員所面臨的排在第一位的挑戰是閱讀和理解其他開發人員的軟件。本文試圖幫助軟件開發團隊來克服這個挑戰。
本文舉出了使得軟件開發團隊更加有效並且更加高質量的五大習慣。首先,本文描述了這樣一些商業團隊給於軟件開發團隊以及他們所開發的軟件的要求。接着,本文解釋了狀態更換邏輯和行爲邏輯之間的重要的區別。最後,本文將要示例五個使用客戶帳戶情形作爲案例研究的習慣。

業務對開發人員提出的要求
業務團隊的工作是要決定往軟件里加入什麼新的功能,同時確保新的功能對業務來說是最有利的。在這裏,“新的功能”是指一個新的產品或者對一個已存在產品的額外的功能提升。換句話說,業務團隊決定了哪些新的特性會幫助軟件產品賺更多的錢。決定上那些新的功能的關鍵因素是實現這些新的功能所需要的成本。如果實現的成本超過了潛在的價值,那麼新的功能將不會實現在軟件產品裏。
業務團隊要求軟件開發團隊以最低的成本開發新的功能。它同時也要求軟件開發團隊在給產品增加新的功能的時候,不會隨着時間的過去而增加新的成本。而且,每一次業務團隊請求開發新的功能,他們要求增加新的功能的同時不會丟失舊的功能。隨着時間的過去,軟件將產生足夠多的功能,業務團隊將要求使用文檔來描述軟件提供的當前功能。然後,業務團隊將使用這些文檔來決定下一個新的功能。
軟件開發團隊通過開發“易於理解”的軟件來最佳的滿足這些要求。“難於理解”的軟件將導致整個開發過程的無效。這些無效增加了軟件開發的成本,並且將導致意想不到的已經實現的功能的丟失。增加了開發人員的開發時間,和錯誤的軟件文檔的交付。通過改變業務團隊的需求,這些無效能夠被減少,將複雜的轉化爲簡單的、“易於理解”的軟件。

引入關鍵概念:狀態和行爲
開發易於理解的軟件開始於創建一個擁有狀態和行爲的對象。“狀態”是指在方法調用之間保持的對象的數據。一個Java對象能夠在它的實例變量中臨時的保持它的狀態,或者通過保存到一個永久的數據源的方式保持。一個永久的數據源可以是數據庫或者Web服務。“狀態改變的方法”典型的是通過獲取數據或從遠程數據源取得和放置數據的方式管理一個對象的數據。“行爲”是一個對象在“狀態”的基礎上回答問題的能力。“行爲方法”回答問題不用改變狀態,並且在應用中通常指的是業務邏輯。

案例研究:CustomerAccount對象
下面的ICustomerAccount接口定義了一個對象必須實現的用來管理用戶帳戶的方法。它定義了產生一個活躍賬戶的能力,載入一個已經存在客戶賬戶狀態、校驗潛在用戶的用戶名和密碼、校驗一個賬戶是否爲活躍賬戶能夠購買產品等功能。

  1.     
  2. public interface ICustomerAccount {   
  3.   // State-changing methods   
  4.   public void createNewActiveAccount() throws CustomerAccountsSystemOutageException;   
  5.   public void loadAccountStatus() throws CustomerAccountsSystemOutageException;   
  6.   // Behavior methods   
  7.   public boolean isRequestedUsernameValid();   
  8.   public boolean isRequestedPasswordValid();   
  9.   public boolean isActiveForPurchasing();   
  10.   public String getPostLogonMessage();   
  11. }  
 
public interface ICustomerAccount {
  // State-changing methods
  public void createNewActiveAccount() throws CustomerAccountsSystemOutageException;
  public void loadAccountStatus() throws CustomerAccountsSystemOutageException;
  // Behavior methods
  public boolean isRequestedUsernameValid();
  public boolean isRequestedPasswordValid();
  public boolean isActiveForPurchasing();
  public String getPostLogonMessage();
}


習慣1:構造器實現最少的工作
第一個習慣是一個對象的構造器只能實現儘量少的工作。理想的,構造器僅僅是通過它的參數載入數據到它的實例變量。如下所是,作爲一個例子,開發一個只實現最少功能的構造器使得這個對象易於被使用和理解。因爲構造器只用來實現簡單任務,將數據載入到對象的實例變量。

  1.     
  2. public class CustomerAccount implements ICustomerAccount {   
  3.   // Instance variables.   
  4.   private String username;   
  5.   private String password;   
  6.   protected String accountStatus;   
  7.   // Constructor that performs minimal work.   
  8.   public CustomerAccount(String username, String password) {   
  9.     this.password = password;   
  10.     this.username = username;   
  11.   }   
  12. }  
 
public class CustomerAccount implements ICustomerAccount {
  // Instance variables.
  private String username;
  private String password;
  protected String accountStatus;
  // Constructor that performs minimal work.
  public CustomerAccount(String username, String password) {
    this.password = password;
    this.username = username;
  }
}


構造器被用來產生對象的實例。構造器的名字永遠和對象的名字相同(譯者:這裏的“對象”應該是“類”的意思,以下相同)。既然構造器的名字是無法改變的,它的名字不能表達出它要行使的其他功能。因而,它最好履行儘量少的工作。另一方面,狀態的改變和行爲方法的名稱使用描述性的名稱來表達它們更加複雜的意圖,就像在“習慣2:方法名要清晰地表達方法的意圖”描述的那樣。下面的例子將要表明:軟件的可讀性很高,因爲構造器僅僅產生對象的實例,而讓行爲和狀態改變的方法去剩下的事情。
注:在例子中使用“…”表示它們在現實案例中是必須的,但是它們與例子的意圖無關

  1. String username = "robertmiller";   
  2. String password = "java.net";   
  3. ICustomerAccount ca = new CustomerAccount(username, password);   
  4. if (ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) {   
  5.   // ...   
  6.   ca.createNewActiveAccount();   
  7.   // ...   
  8. }  
    String username = "robertmiller";
    String password = "java.net";
    ICustomerAccount ca = new CustomerAccount(username, password);
    if (ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) {
      // ...
      ca.createNewActiveAccount();
      // ...
    }



另一方面,那些構造器的功能超過載入實例變量的對象是難於理解的,並且容易被誤用,因爲它們的名稱沒有傳遞它們的意圖。例如,下面的這個構造器額外的調用一個方法用來遠程調用數據庫或者Web服務,目的是爲了事先載入一個賬戶的狀態。

  1. // Constructor that performs too much work!   
  2. public CustomerAccount(String username, String password) throws CustomerAccountsSystemOutageException {   
  3.   this.password = password;   
  4.   this.username = username;   
  5.   this.loadAccountStatus();   
  6.   // unnecessary work.   
  7. }   
  8. // Remote call to the database or web service.   
  9. public void loadAccountStatus() throws CustomerAccountsSystemOutageException { // ...   
  10. }  
 
  // Constructor that performs too much work!
  public CustomerAccount(String username, String password) throws CustomerAccountsSystemOutageException {
    this.password = password;
    this.username = username;
    this.loadAccountStatus();
    // unnecessary work.
  }
  // Remote call to the database or web service.
  public void loadAccountStatus() throws CustomerAccountsSystemOutageException { // ...
  }



一個開發人員使用了這個構造器,但他沒有意識到構造器已經做了一個遠程調用,因而做了兩個遠程調用。

  1. String username = "robertmiller";   
  2. String password = "java.net";   
  3. try { // makes a remote call   
  4.   ICustomerAccount ca = new CustomerAccount(username, password);   
  5.   // makes a second remote call   
  6.   ca.loadAccountStatus();   
  7. catch (CustomerAccountsSystemOutageException e) { // ...   
  8. }  
    String username = "robertmiller";
    String password = "java.net";
    try { // makes a remote call
      ICustomerAccount ca = new CustomerAccount(username, password);
      // makes a second remote call
      ca.loadAccountStatus();
    } catch (CustomerAccountsSystemOutageException e) { // ...
    }



或者一個開發人員重用了這個構造器來校驗一個潛在的用戶所希望的用戶名和密碼,那麼他就被迫作出了一個不需要的遠程調用,因爲這些方法(isRequestedUsernameValid(), isRequestedPasswordValid())不需要賬戶狀態。

  1. String username = "robertmiller";   
  2. String password = "java.net";   
  3. try { // makes unnecessary remote call   
  4.   ICustomerAccount ca = new CustomerAccount(username, password);   
  5.   if (ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) { // ...   
  6.     ca.createNewActiveAccount(); // ...   
  7.   }   
  8. catch (CustomerAccountsSystemOutageException e) { // ...   
  9. }  
    String username = "robertmiller";
    String password = "java.net";
    try { // makes unnecessary remote call
      ICustomerAccount ca = new CustomerAccount(username, password);
      if (ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid()) { // ...
        ca.createNewActiveAccount(); // ...
      }
    } catch (CustomerAccountsSystemOutageException e) { // ...
    }



習慣2:方法名清晰的表達方法的意圖
第二個習慣是,通過它們的方法名,所有的方法必須清晰的傳遞它們的意圖。例如,isRequestedUsernameValid()讓開發人員知道這個方法確定請求的用戶名是否是合法的。與之相對照的是,isGoodUser()可能有好幾種用途:它能決定一個用戶的賬戶是否是活躍的,決定是否請求的用戶名或密碼是正確的,或者決定用戶是否是一個好人。既然這個方法名的描述性不強,那麼它對於其他開發人員來說很難確定它的意圖是什麼。簡短地說,一個方法名使用長的和描述性的比使用短的和毫無意義的好。
長的、描述性的方法名幫助開發團隊迅速的理解他們的軟件的意圖和功能。此外,應用這種技術到測試方法的名稱,使得測試表達了軟件現有的需求。例如,軟件要求檢驗請求的用戶名和密碼是不同的。使用方法名稱:testRequestedPasswordIsNotValidBecauseItMustBeDifferentThanTheUsername()就能傳遞測試的這個意圖,因此爲表達了軟件需求的意圖。

  1. public class CustomerAccountTest extends TestCase{    
  2.   public void testRequestedPasswordIsNotValid    BecauseItMustBeDifferentThanTheUsername(){    
  3.     String username = "robertmiller";       
  4.     String password = "robertmiller";      
  5.     ICustomerAccount ca = new CustomerAccount(username, password);    
  6.     assertFalse(ca.isRequestedPasswordValid());    
  7.   }   
  8. }   
public class CustomerAccountTest extends TestCase{ 
  public void testRequestedPasswordIsNotValid    BecauseItMustBeDifferentThanTheUsername(){ 
    String username = "robertmiller";    
    String password = "robertmiller";   
    ICustomerAccount ca = new CustomerAccount(username, password); 
    assertFalse(ca.isRequestedPasswordValid()); 
  }
} 



這個測試方法可以很簡單的被命名爲testRequestedPasswordIsNotValid(),或者更差的testBadPassword(),這兩個名稱都使得它很難確定測試的意圖。不清楚地或者說含糊不清的名稱將導致效率的損失。效率的損失導致需要增加額外的時間來理解測試、創建不需要的方法或屬性、重複的或者衝突的測試、或者銷燬了對象已經測試過的已經存在的功能。
最後,描述性的方法名減少了對於常規文檔或者Javadoc註釋的需要。

習慣3:一個對象執行功能集中的服務集
第三個習慣是,軟件的每一個對象都集中的執行一個小的、獨一無二的服務集。執行小基數工作的對象容易閱讀,容易正確使用,因爲只有少量的代碼需要理解。此外,軟件的每一個對象都必須執行獨一無二的服務集,因爲重複的邏輯浪費開發人員的時間,增加維護的成本。設想,未來,業務團隊要求升級isRequestedPasswordValid()的邏輯,如果兩個不同的對象都有執行相同動作的類似方法,在這種情況下,軟件開發人員將要比升級僅僅一個對象花費更多的時間來升級兩個對象。
作爲案例學習的示例,CustomerAccount對象的目的是管理一個獨立的客戶的賬戶。它首先是創建一個賬戶,然後是驗證賬戶對於購買商品來說仍然是活躍的。假設在未來,軟件需要給那些購買了十件以上商品的客戶折扣。創建一個新的接口,ICustomerTransactions,而且對象,CustomerTransactions,來實現這些新的特性。這些都是開發“易於理解”軟件需要有目的進行的工作。

  1.     
  2. public interface ICustomerTransactions {   
  3.   // State-changing methods   
  4.   public void createPurchaseRecordForProduct(Long productId) throws CustomerTransactionsSystemException;   
  5.   public void loadAllPurchaseRecords() throws CustomerTransactionsSystemException;   
  6.   // Behavior // method   
  7.   public void isCustomerEligibleForDiscount();   
  8. }  
 
public interface ICustomerTransactions {
  // State-changing methods
  public void createPurchaseRecordForProduct(Long productId) throws CustomerTransactionsSystemException;
  public void loadAllPurchaseRecords() throws CustomerTransactionsSystemException;
  // Behavior // method
  public void isCustomerEligibleForDiscount();
}


這個新的對象維護存儲客戶交易和決定什麼時候客戶獲得他的十件商品者扣的狀態變化和行爲方法。它應該是易於創建、測試和維護,因爲它只有一個簡單的、集中的目的。而一個效率低下的方法是將這些新方法加入到已經存在的ICustomerAccount接口和CustomerAccount對象。如下所示:

  1.     
  2. public interface ICustomerAccount {   
  3.   // State-changing methods   
  4.   public void createNewActiveAccount() throws CustomerAccountsSystemOutageException;   
  5.   public void loadAccountStatus() throws CustomerAccountsSystemOutageException;   
  6.   public void createPurchaseRecordForProduct(Long productId) throws CustomerAccountsSystemOutageException;   
  7.   public void loadAllPurchaseRecords() throws CustomerAccountsSystemOutageException;   
  8.   // Behavior methods   
  9.   public boolean isRequestedUsernameValid();   
  10.   public boolean isRequestedPasswordValid();   
  11.   public boolean isActiveForPurchasing();   
  12.   public String getPostLogonMessage();   
  13.   public void isCustomerEligibleForDiscount();   
  14. }  
 
public interface ICustomerAccount {
  // State-changing methods
  public void createNewActiveAccount() throws CustomerAccountsSystemOutageException;
  public void loadAccountStatus() throws CustomerAccountsSystemOutageException;
  public void createPurchaseRecordForProduct(Long productId) throws CustomerAccountsSystemOutageException;
  public void loadAllPurchaseRecords() throws CustomerAccountsSystemOutageException;
  // Behavior methods
  public boolean isRequestedUsernameValid();
  public boolean isRequestedPasswordValid();
  public boolean isActiveForPurchasing();
  public String getPostLogonMessage();
  public void isCustomerEligibleForDiscount();
}



就像上面看到的那樣,允許對象變成大的責任和目標的倉庫將使得它們更加難以閱讀,更加容易誤解。誤解將導致效率的損失,增加業務團隊的時間和金錢。簡而言之,讓對象和它的方法集中的執行小單元的工作更好一些。

習慣4:狀態改變方法包含最小限度的行爲邏輯
第四個習慣是狀態改變方法必須包含最小數量的行爲邏輯。混合狀態改變邏輯和行爲邏輯使得軟件理解起來更加的困難,因爲它增加了在一個地方發生的工作的數量。狀態改變方法通常是用來獲取或發送數據到一個遠程的數據存儲設備,因而容易在產品系統中出現問題。診斷一個狀態改變方法的系統問題在遠程調用被獨立的時候更容易一些,這時候它完全不含有行爲邏輯。此外,兩者的混合還制約了開發過程。例如,getPostLogonMessage()是一個基於accountStatus的值的行爲方法:

  1. public String getPostLogonMessage() {   
  2.   if ("A".equals(this.accountStatus)) {   
  3.     return "Your purchasing account is active.";   
  4.   } else if ("E".equals(this.accountStatus)) {   
  5.     return "Your purchasing account has " + "expired due to a lack of activity.";   
  6.   } else {   
  7.     return "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";   
  8.   }   
  9. }  
  public String getPostLogonMessage() {
    if ("A".equals(this.accountStatus)) {
      return "Your purchasing account is active.";
    } else if ("E".equals(this.accountStatus)) {
      return "Your purchasing account has " + "expired due to a lack of activity.";
    } else {
      return "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";
    }
  }



loadAccountStatus()是從遠程數據存儲設備載入accountStatus的值的狀態改變方法:

  1. public void loadAccountStatus() throws CustomerAccountsSystemOutageException {   
  2.   Connection c = null;   
  3.   try {   
  4.     c = DriverManager.getConnection("databaseUrl""databaseUser""databasePassword");   
  5.     PreparedStatement ps = c   
  6.         .prepareStatement("SELECT status FROM customer_account " + "WHERE username = ? AND password = ? ");   
  7.     ps.setString(1this.username);   
  8.     ps.setString(2this.password);   
  9.     ResultSet rs = ps.executeQuery();   
  10.     if (rs.next()) {   
  11.       this.accountStatus = rs.getString("status");   
  12.     }   
  13.     rs.close();   
  14.     ps.close();   
  15.     c.close();   
  16.   } catch (SQLException e) {   
  17.     throw new CustomerAccountsSystemOutageException(e);   
  18.   } finally {   
  19.     if (c != null) {   
  20.       try {   
  21.         c.close();   
  22.       } catch (SQLException e) {}   
  23.     }   
  24.   }   
  25. }  
  public void loadAccountStatus() throws CustomerAccountsSystemOutageException {
    Connection c = null;
    try {
      c = DriverManager.getConnection("databaseUrl", "databaseUser", "databasePassword");
      PreparedStatement ps = c
          .prepareStatement("SELECT status FROM customer_account " + "WHERE username = ? AND password = ? ");
      ps.setString(1, this.username);
      ps.setString(2, this.password);
      ResultSet rs = ps.executeQuery();
      if (rs.next()) {
        this.accountStatus = rs.getString("status");
      }
      rs.close();
      ps.close();
      c.close();
    } catch (SQLException e) {
      throw new CustomerAccountsSystemOutageException(e);
    } finally {
      if (c != null) {
        try {
          c.close();
        } catch (SQLException e) {}
      }
    }
  }



單元測試方法getPostLogonMessage()能夠通過模仿loadAccountStatus()方法很容易地進行測試,不需要那種通過數據庫的遠程調用,每一個假設條件就能夠被測試到。例如,accountStatus是“E”用來中止,那麼getPostLogonMessage()將返回“Your purchasing account has expired due to a lack of activity,”,如下所示:

  1. public void testPostLogonMessageWhenStatusIsExpired() {   
  2.   String username = "robertmiller";   
  3.   String password = "java.net";   
  4.   class CustomerAccountMock extends CustomerAccount { // ...   
  5.     public void loadAccountStatus() {   
  6.       this.accountStatus = "E";   
  7.     }   
  8.   }   
  9.   ICustomerAccount ca = new CustomerAccountMock(username, password);   
  10.   try {   
  11.     ca.loadAccountStatus();   
  12.   } catch (CustomerAccountsSystemOutageException e) {   
  13.     fail("" + e);   
  14.   }   
  15.   assertEquals("Your purchasing account has " + "expired due to a lack of activity.", ca.getPostLogonMessage());   
  16. }  
  public void testPostLogonMessageWhenStatusIsExpired() {
    String username = "robertmiller";
    String password = "java.net";
    class CustomerAccountMock extends CustomerAccount { // ...
      public void loadAccountStatus() {
        this.accountStatus = "E";
      }
    }
    ICustomerAccount ca = new CustomerAccountMock(username, password);
    try {
      ca.loadAccountStatus();
    } catch (CustomerAccountsSystemOutageException e) {
      fail("" + e);
    }
    assertEquals("Your purchasing account has " + "expired due to a lack of activity.", ca.getPostLogonMessage());
  }



與之相反的方法是將getPostLogonMessage()的行爲邏輯和loadAccountStatus()的狀態改變工作放到一個方法裏。下面的示例展示了這個錯誤的做法:

  1. public String getPostLogonMessage() {   
  2.   return this.postLogonMessage;   
  3. }   
  4. public void loadAccountStatus() throws CustomerAccountsSystemOutageException {   
  5.   Connection c = null;   
  6.   try {   
  7.     c = DriverManager.getConnection("databaseUrl""databaseUser""databasePassword");   
  8.     PreparedStatement ps = c   
  9.         .prepareStatement("SELECT status FROM customer_account " + "WHERE username = ? AND password = ? ");   
  10.     ps.setString(1this.username);   
  11.     ps.setString(2this.password);   
  12.     ResultSet rs = ps.executeQuery();   
  13.     if (rs.next()) {   
  14.       this.accountStatus = rs.getString("status");   
  15.     }   
  16.     rs.close();   
  17.     ps.close();   
  18.     c.close();   
  19.   } catch (SQLException e) {   
  20.     throw new CustomerAccountsSystemOutageException(e);   
  21.   } finally {   
  22.     if (c != null) {   
  23.       try {   
  24.         c.close();   
  25.       } catch (SQLException e) {}   
  26.     }   
  27.   }   
  28.   if ("A".equals(this.accountStatus)) {   
  29.     this.postLogonMessage = "Your purchasing account is active.";   
  30.   } else if ("E".equals(this.accountStatus)) {   
  31.     this.postLogonMessage = "Your purchasing account has " + "expired due to a lack of activity.";   
  32.   } else {   
  33.     this.postLogonMessage = "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";   
  34.   }   
  35. }  
 
  public String getPostLogonMessage() {
    return this.postLogonMessage;
  }
  public void loadAccountStatus() throws CustomerAccountsSystemOutageException {
    Connection c = null;
    try {
      c = DriverManager.getConnection("databaseUrl", "databaseUser", "databasePassword");
      PreparedStatement ps = c
          .prepareStatement("SELECT status FROM customer_account " + "WHERE username = ? AND password = ? ");
      ps.setString(1, this.username);
      ps.setString(2, this.password);
      ResultSet rs = ps.executeQuery();
      if (rs.next()) {
        this.accountStatus = rs.getString("status");
      }
      rs.close();
      ps.close();
      c.close();
    } catch (SQLException e) {
      throw new CustomerAccountsSystemOutageException(e);
    } finally {
      if (c != null) {
        try {
          c.close();
        } catch (SQLException e) {}
      }
    }
    if ("A".equals(this.accountStatus)) {
      this.postLogonMessage = "Your purchasing account is active.";
    } else if ("E".equals(this.accountStatus)) {
      this.postLogonMessage = "Your purchasing account has " + "expired due to a lack of activity.";
    } else {
      this.postLogonMessage = "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";
    }
  }



在這個實現裏,行爲方法getPostLogonMessage()沒有包含任何的行爲邏輯,而是簡單的返回實例變量this.postLogonMessage。這個實現存在着三個問題。首先,這個實現使得我們很難理解“post logon message”的邏輯是怎麼工作的,因爲它被包含在一個執行兩個任務的方法裏。第二,getPostLogonMessage()的重用是受限制的,因爲它永遠和loadAccountStatus()相關聯。最後,在出現系統問題的情況下,CustomerAccountsSystemOutageException將會被拋出,使得方法在設置this.postLogonMessage的值之前就停止了。這個實現也對測試產生了一個負面的影響,因爲測試getPostLogonMessage()邏輯的唯一方法是創建一個CustomerAccount對象,這個對象有一個在數據庫裏有用戶名和密碼的用戶,而且這個用戶的accountStatus被設置爲“E”,被用來停止。這將導致爲了這個測試必須給數據庫做一個遠程調用。這使得這個測試運行起來速度慢,而且由於數據庫發生的改變將導致測試意想不到的失敗。這個測試需要對數據庫做一個遠程調用,因爲loadAccountStatus()方法也包含了行爲邏輯,如果行爲邏輯被模仿,那麼測試測試的是模擬對象的行爲,而不是實際對象的行爲。

習慣5:行爲方法能夠在任何條件下被調用
第五個習慣是確保每一個行爲方法提供的功能相對於其他的行爲方法來說是獨立的。換句話說,一個對象的行爲方法能夠被重複和以任何順利調用。這個習慣使得對象傳遞固定的行爲。例如,CustomerAccount對象的isActiveForPurchasing()和getPostLogonMessage()行爲方法在它們的邏輯裏都使用accountStatus的值。每一個方法對於其他的方法來說是功能獨立的。例如,一個場景要求isActiveForPurchasing()被調用,接着調用getPostLogonMessage():

  1. ICustomerAccount ca = new CustomerAccount(username, password);   
  2. ca.loadAccountStatus();   
  3. if (ca.isActiveForPurchasing()) { // go to "begin purchasing" display   
  4.   // ..   
  5.   // show post logon message.   
  6.   ca.getPostLogonMessage();   
  7. else {   
  8.   // go to "activate account" display   
  9.   // ...   
  10.   // show post logon message. ca.getPostLogonMessage();   
  11. }  
    ICustomerAccount ca = new CustomerAccount(username, password);
    ca.loadAccountStatus();
    if (ca.isActiveForPurchasing()) { // go to "begin purchasing" display
      // ..
      // show post logon message.
      ca.getPostLogonMessage();
    } else {
      // go to "activate account" display
      // ...
      // show post logon message. ca.getPostLogonMessage();
    }



另一個場景要求調用getPostLogonMessage(),而不要求調用isActiveForPurchasing():

  1. ICustomerAccount ca = new CustomerAccount(username, password);   
  2. ca.loadAccountStatus();   
  3. // go to "welcome back" display   
  4. // ...   
  5. // show post logon message.ca.getPostLogonMessage();  
    ICustomerAccount ca = new CustomerAccount(username, password);
    ca.loadAccountStatus();
    // go to "welcome back" display
    // ...
    // show post logon message.ca.getPostLogonMessage();



如果getPostLogonMessage()要求isActiveForPurchasing()首先被調用的話,CustomerAccount對象將不支持第二個場景。例如,創建兩個方法來使用一個postLogonMessage實例變量,這樣,它的值能夠在支持場景一的方法中間得到維護,但是在支持場景二的方法中卻不能:

  1. public boolean isActiveForPurchasing() {   
  2.   boolean returnValue = false;   
  3.   if ("A".equals(this.accountStatus)) {   
  4.     this.postLogonMessage = "Your purchasing account is active.";   
  5.     returnValue = true;   
  6.   } else if ("E".equals(this.accountStatus)) {   
  7.     this.postLogonMessage = "Your purchasing account has " + "expired due to a lack of activity.";   
  8.     returnValue = false;   
  9.   } else {   
  10.     this.postLogonMessage = "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";   
  11.     returnValue = false;   
  12.   }   
  13.   return returnValue;   
  14. }   
  15. public String getPostLogonMessage() {   
  16.   return this.postLogonMessage;   
  17. }  
 
  public boolean isActiveForPurchasing() {
    boolean returnValue = false;
    if ("A".equals(this.accountStatus)) {
      this.postLogonMessage = "Your purchasing account is active.";
      returnValue = true;
    } else if ("E".equals(this.accountStatus)) {
      this.postLogonMessage = "Your purchasing account has " + "expired due to a lack of activity.";
      returnValue = false;
    } else {
      this.postLogonMessage = "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";
      returnValue = false;
    }
    return returnValue;
  }
  public String getPostLogonMessage() {
    return this.postLogonMessage;
  }



另一方面,如果兩個方法設計爲邏輯彼此相互獨立,那麼它們將支持兩個場景。在這個優勝的例子中,postLogonMessage是一個局域變量,由getPostLogonMessage()方法自己創建:

  1. public boolean isActiveForPurchasing() {   
  2.   return this.accountStatus != null && this.accountStatus.equals("A");   
  3. }   
  4. public String getPostLogonMessage() {   
  5.   if ("A".equals(this.accountStatus)) {   
  6.     return "Your purchasing account is active.";   
  7.   } else if ("E".equals(this.accountStatus)) {   
  8.     return "Your purchasing account has " + "expired due to a lack of activity.";   
  9.   } else {   
  10.     return "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";   
  11.   }   
  12. }  
 
  public boolean isActiveForPurchasing() {
    return this.accountStatus != null && this.accountStatus.equals("A");
  }
  public String getPostLogonMessage() {
    if ("A".equals(this.accountStatus)) {
      return "Your purchasing account is active.";
    } else if ("E".equals(this.accountStatus)) {
      return "Your purchasing account has " + "expired due to a lack of activity.";
    } else {
      return "Your purchasing account cannot be " + "found, please call customer service " + "for assistance.";
    }
  }



使得這兩個方法彼此獨立的一個額外的好處是這些方法容易被理解。例如,isActiveForPurchasing()更加具有可讀性,因爲它僅僅只回答“is active for purchasing”的問題,而不是相反的它還要設置“post logon message”。另外一個額外的好處是每一個方法都能被獨立的測試,這也使得測試容易被理解:

  1.     
  2. public class CustomerAccountTest extends TestCase {   
  3.   public void testAccountIsActiveForPurchasing() {   
  4.     String username = "robertmiller";   
  5.     String password = "java.net";   
  6.     class CustomerAccountMock extends CustomerAccount { // ...   
  7.       public void loadAccountStatus() {   
  8.         this.accountStatus = "A";   
  9.       }   
  10.     }   
  11.     ICustomerAccount ca = new CustomerAccountMock(username, password);   
  12.     try {   
  13.       ca.loadAccountStatus();   
  14.     } catch (CustomerAccountsSystemOutageException e) {   
  15.       fail("" + e);   
  16.     }   
  17.     assertTrue(ca.isActiveForPurchasing());   
  18.   }   
  19.   public void testGetPostLogonMessageWhenAccountIsActiveForPurchasing() {   
  20.     String username = "robertmiller";   
  21.     String password = "java.net";   
  22.     class CustomerAccountMock extends CustomerAccount { // ...   
  23.       public void loadAccountStatus() {   
  24.         this.accountStatus = "A";   
  25.       }   
  26.     }   
  27.     ICustomerAccount ca = new CustomerAccountMock(username, password);   
  28.     try {   
  29.       ca.loadAccountStatus();   
  30.     } catch (CustomerAccountsSystemOutageException e) {   
  31.       fail("" + e);   
  32.     }   
  33.     assertEquals("Your purchasing account is active.", ca.getPostLogonMessage());   
  34.   }   
  35. }  
 
public class CustomerAccountTest extends TestCase {
  public void testAccountIsActiveForPurchasing() {
    String username = "robertmiller";
    String password = "java.net";
    class CustomerAccountMock extends CustomerAccount { // ...
      public void loadAccountStatus() {
        this.accountStatus = "A";
      }
    }
    ICustomerAccount ca = new CustomerAccountMock(username, password);
    try {
      ca.loadAccountStatus();
    } catch (CustomerAccountsSystemOutageException e) {
      fail("" + e);
    }
    assertTrue(ca.isActiveForPurchasing());
  }
  public void testGetPostLogonMessageWhenAccountIsActiveForPurchasing() {
    String username = "robertmiller";
    String password = "java.net";
    class CustomerAccountMock extends CustomerAccount { // ...
      public void loadAccountStatus() {
        this.accountStatus = "A";
      }
    }
    ICustomerAccount ca = new CustomerAccountMock(username, password);
    try {
      ca.loadAccountStatus();
    } catch (CustomerAccountsSystemOutageException e) {
      fail("" + e);
    }
    assertEquals("Your purchasing account is active.", ca.getPostLogonMessage());
  }
}



結論
遵從上面的五個習慣將有助於開發團隊開發的軟件容易被團隊的每一個成員閱讀、理解和修改。當軟件開發團隊開發新的功能太快,而不考慮將來,那麼他們的軟件將增加高額的使用成本。不可避免的,當他們爲了在一次的理解和修改軟件的時候,他們將得到的是可怕的經歷。如果軟件很難被理解的話,增加一個新的功能就會變得非常昂貴。然而,當開發團隊應用這些好的經驗,他們將以最低的成本給業務團隊提供一個新的功能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章