原文地址:
http://m.tkk7.com/chords/archive/2006/12/14/87591.htmlFive Habits of Highly Profitable Software Developers
by
Robert J. Miller08/24/2006
翻譯:Coody Sk8er
http://m.tkk7.com/chords原文地址:
http://today.java.net/pub/a/today/2006/08/24/five-habits-of-highly-profitable-developers.html
當今技術引領經濟社會大量的需要能夠在團隊環(huán)境中開發(fā)出穩(wěn)定質量的軟件開發(fā)人員。在團隊開發(fā)的環(huán)境中,開發(fā)者面對的挑戰(zhàn)就是讀懂別的開發(fā)者寫的軟件。本文將文章盡力幫助軟件開發(fā)團隊來克服這樣的困難。
本文為了闡明了五個讓開發(fā)團隊變得比以往更加高效的好習慣,首先將介紹公司業(yè)務對開發(fā)團隊以及他們開發(fā)出軟件的需求,接下來會解釋狀態(tài)改變邏輯和行為邏輯之間重要的區(qū)別,最后會通過顧客賬號這么一個案例來闡述這五個習慣。
業(yè)務帶給開發(fā)人員的需求
公司業(yè)務團隊的工作就是在決定將哪些對公司業(yè)務最有利的新價值可以被加到軟件中。這里的“新價值”指的是新產品或者是對現有產品的強化。換句話說就是,業(yè)務團隊決定什么將給公司帶來最多的錢。決定了下個新價值是什么的關鍵因素是實現它的成本。如果實現的成本超過了潛在收益,那么這個新價值就不會被加到軟件中來。
業(yè)務團隊要求開發(fā)團隊能夠盡可能低成本的,并且是在規(guī)定時間內以及在不失去原有價值的情況下創(chuàng)造新價值。當軟件增加了一定價值后,業(yè)務團隊會要求一份描述現有軟件所能提供的價值的文檔。這個文檔將幫助他們決定下一個新價值是什么。
軟件開發(fā)團隊通過創(chuàng)造出容易理解的軟件來滿足商業(yè)團隊的需求。難以理解的軟件帶來的后果就是整個開發(fā)過程的低效率。低效率會造成軟件開發(fā)成本的增加,引起一些預料不到的現有價值的損失,開發(fā)周期滾雪球般越拖越長以及交付錯誤的軟件文檔。通過改變業(yè)務團隊的需求,甚至將復雜的軟件轉變成簡單、容易理解的軟件,就可以提高開發(fā)過程的效率。
介紹關鍵概念:狀態(tài)和行為
開發(fā)容易理解的軟件可以從創(chuàng)建有狀態(tài)和行為的對象開始。“狀態(tài)”是對象在調用方法前后所保存的數據。一個JAVA對象的實例變量可以暫時的保持自己的狀態(tài),并且可以隨時存放到數據存儲器里。這里,永久數據存儲器可以是數據庫或者是Web服務。“狀態(tài)變更方法”主要管理一個對象的數據存取。“行為”則是一個對象基于狀態(tài)回答問題的能力。“行文方法”回答問題永遠不會修改狀態(tài),并且這些方法往往跟一個應用的商業(yè)邏輯有關。
案例研究:CustomerAccount對象
下面這個ICustomerAccount接口定義了管理一個客戶賬號對象必須實現的功能。這個接口定義了可以創(chuàng)建一個新的賬號,加載一個已經存在的賬號的信息,驗證某個賬號的用戶名和密碼,驗證購買時這個賬號是否是激活的。

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();
}
習慣一:構造器盡量少做事
第一個應該養(yǎng)成的喜歡就是讓類的構造器盡量的少做些事情。理想的情況就是構造器僅僅用來接受參數給實例變量加載數據。下面一個例子,讓構造器做盡可能少的事情會讓這個類使用起來比較簡單,因為構造器只是簡單的給類中的實例變量賦值。

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;
}
}
構造器是用來創(chuàng)建一個類的實例。構造器的名字永遠是跟這個類的名字是一樣的。既然構造器的名字無法改變,那么它就不能表達出它做的事情的含義。所以,最好是盡可能的讓構造器少做點事。另一個方面,狀態(tài)變更方法和行為方法會通過自己的名字來表達出自己復雜的工作,在“習慣二:方法名要清晰的表現意圖”中會詳細講到。下一個例子表明,很大程度上是因為構造器十分的簡單,更多的讓狀態(tài)變更和行為方法來完成其他的部分,使得一個軟件具有很高的可讀性。
注:例子中“...”部分僅僅是真實情景中必須的部分,跟本文要闡述的問題沒有關系。
String username = "robertmiller";
String password = "java.net";
ICustomerAccount ca = new CustomerAccount(username, password);

if(ca.isRequestedUsernameValid() && ca.isRequestedPasswordValid())
{

ca.createNewActiveAccount();

}
相反的,如果構造器除了給實例變量賦值以外的事情,將會使代碼很難讓人理解,并且有可能被誤用,因為構造器的名字沒有說明要做的意圖。例如,下面的代碼將調用數據庫或者Web服務來預加載賬號的狀態(tài):
//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
{

}
別人可能在不知道會使用遠程調的情況下使用這個構造器,從而導致了以下個遠程調用:
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)
{

}
或者使開發(fā)人員重用這個構造器來驗證用戶名和密碼,并且被強制的進行了遠程調用,然而這些行為方法(isRequestedUsernameValid(), isRequestedPasswordValid())根本不需要賬戶的狀態(tài):
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)
{

}
習慣二:方法名要清晰的表現意圖
第二個習慣就是要讓所有的方法名字清晰的表現本方法要做什么的意圖。例如isRequestedUsernameValid()讓開發(fā)人員知道這個方法時用來驗證用戶名是否正確的。相反的,isGoodUser() 可能有很多用途:用來驗證賬號是否是激活的,用來驗證用戶名或者密碼是否正確,或者是用來搞清楚用戶是不是個好人。方法名表意不清,這就很難讓開發(fā)者明白這個方法到底是用來干什么的。簡單的說,長一點并且表意清晰的方法名要比又短又表意不明的方法名好。
表意清晰的長名字會幫助開發(fā)團隊快速的理解他們軟件的功能意圖。更大的優(yōu)點在于,給測試方法也起個好名字會讓軟件現有的要求更加的清晰。例如,本軟件要求驗證請求的用戶名和用戶密碼是不同的。使用名為 testRequestedPasswordIsNotValidBecauseItMustBeDifferentThanTheUsername() 的方法清晰的表達出了方法的意圖,也就是軟件要達到的要求。
import junit.framework.TestCase;


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()。這兩個名字都讓人很難搞清楚這個方法是用來測試什么的。不清楚或者是模棱兩可的測試方法名會帶來生產力的損失。從而導致花費來越多的時間來理解測試,不必要的重復測試,或者是破壞了被測試的類。
最后,明了的方法名還能減少文檔和注釋的工作量。
習慣三:一個對象只進行一類服務。
第三個喜歡就是對象只關心處理一小類獨立的服務。一個對象只處理一小部分事情將使得代碼更好讀好用,因為每個對象代碼量很少。更糟糕的是,重復的邏輯將花費很多時間和成本去維護。設想一下,業(yè)務部門將來要求升級一下isRequestedPasswordValid()里的邏輯,然而有兩個不同的對象卻有著功能完全一樣但是名字不一樣的方法。這種情況下,開發(fā)團隊要花費更多的時間去更新兩個對象,而不是一個。
這個案例表明了 CustomerAccount類的目的就是管理一個客戶的帳號。它首先創(chuàng)建了一個帳號,然后嚴整這個帳號能否用來購買產品。假設軟件要給所有購買過10件物品以上的客戶打折。再創(chuàng)建一個接口叫ICustomerTransactions和一個叫CustomerTransactions的類,這樣會讓代碼更加易懂,并且實現目標。

public interface ICustomerTransactions
{
//State-changing methods
public void createPurchaseRecordForProduct(Long productId)
throws CustomerTransactionsSystemException;
public void loadAllPurchaseRecords()
throws CustomerTransactionsSystemException;
//Behavior method
public void isCustomerEligibleForDiscount();
}
這個新的類里面有狀態(tài)變更和行為方法,可以儲存客戶的交易并且判斷是否能夠打折。這個類創(chuàng)建起來十分簡單,方便測試以及穩(wěn)定,因為它專注心這一個目標。一個低效率的方法是如同下面的例子一樣在ICustomerAccount接口和CustomerAccount類加上新的方法:

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();
}

就像是上面所看到的一樣,這樣使得類具有太多職責,難以讀懂,甚至更容被易誤解。代碼被誤解的后果就是降低生產力,費時費力。總的來說,最好讓一個對象和它的方法集中處理一個小的工作單元。
習慣四:狀態(tài)變更方法少含有行為邏輯
第四個習慣是讓狀態(tài)變更方法少含有行為邏輯。混合了狀態(tài)變更邏輯和行為邏輯的代碼讓人很難理解,因為在一個地方處理了太多的事情。狀態(tài)變更方法涉及到遠程調用來存儲數據的話很容易產生系統(tǒng)問題。如果遠程方法是相對獨立的,并且方法本身沒有行為邏輯,這樣診斷起狀態(tài)改變方法就會十分容易。另外一個問題是,混合了行為邏輯的狀態(tài)代碼很難進行單元測試。例如,getPostLogonMessage() 是一個依靠accountStatus的值的行為:

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值的狀態(tài)改變方法。
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"

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()的狀態(tài)轉變都放到了一個方法里,我們不應該這么做:

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()方法相關聯(lián)。最后,CustomerAccountsSystemOutageException異常將會拋出,導致了在給this.postLogonMessage賦值前就退出方法了。
這個實現同樣創(chuàng)造了負面效應,因為只有創(chuàng)建一個存在于數據庫的CustomerAccount對象,并且將賬號狀態(tài)設置成E才能進行對getPostLogonMessage()邏輯的單元測試。結果式這個測試要進行遠程調用。這會導致測試的很慢,而且在改變數據庫內容的時候很容易出意想不到的問題。由于 loadAccountStatus()方法包含了行為邏輯,測試必須進行遠程調用。如果行為邏輯測試失敗了,測的只是那個失敗的對象行為,而不是真正的對象的行為。
習慣五:可以任意次序調用行為方法第五個習慣是要保證每個行為方法之間保持著獨立。換句話說,一個對象的行為方法可以被重復或任何次序來調用。這個習慣能讓對象實現穩(wěn)定的行為。比如, CustomerAccount's isActiveForPurchasing()和getPostLogonMessage() 行為方法都要用到accountStatus的值。這兩個方法必須在功能上相互獨立。例如,有一個情景要求調用 isActiveForPurchasing(),接著又調用了getPostLogonMessage():
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();
}
一個發(fā)送的情節(jié)會要求調用getPostLogonMessage()之前不必調用isActiveForPurchasing():
ICustomerAccount ca = new CustomerAccount(username, password);
ca.loadAccountStatus();
//go to "welcome back" display


//show post logon message.
ca.getPostLogonMessage();
如果要求調用getPostLogonMessage()之前必須調用isActiveForPurchasing()方法, CustomerAccount 對象將無法支持第二個情景。如果兩個方法使用了 postLogonMessage 實例變量來存放兩個方法所需要的值,那么這將支持第一個情景,但不支持第二個:

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()創(chuàng)建的一個局部變量。

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()如果只是用來回答如“能否購買”的問題會顯得可讀性更佳,如果是用來解決“顯示登陸消息”就不那么好了。另一個好處就是測試是獨立的,讓測試更加簡單和容易理解:

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());
}
}
總結
上述的五種習慣會幫助開發(fā)團隊創(chuàng)造出方便閱讀、理解和修改的軟件。如果開發(fā)團隊僅僅是想快速的創(chuàng)造價值而不考慮將來的規(guī)劃,他們軟件的實現將會耗費越來越多的成本。當這些開發(fā)團隊要審查軟件來理解和修改時,不可避免的會遭到自己寫的壞代碼的報復。如果軟件十分難以理解,在增加新價值的時候會花費巨大的代價。然而,一旦開發(fā)團隊將良好的習慣運用到開發(fā)實踐中,他們會以最低的成本為業(yè)務提供新價值。