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

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

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

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

ca.createNewActiveAccount();

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

}
別人可能在不知道會使用遠(yuǎn)程調(diào)的情況下使用這個(gè)構(gòu)造器,從而導(dǎo)致了以下個(gè)遠(yuǎn)程調(diào)用:
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ā)人員重用這個(gè)構(gòu)造器來驗(yàn)證用戶名和密碼,并且被強(qiáng)制的進(jìn)行了遠(yuǎn)程調(diào)用,然而這些行為方法(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)
{

}
習(xí)慣二:方法名要清晰的表現(xiàn)意圖
第二個(gè)習(xí)慣就是要讓所有的方法名字清晰的表現(xiàn)本方法要做什么的意圖。例如isRequestedUsernameValid()讓開發(fā)人員知道這個(gè)方法時(shí)用來驗(yàn)證用戶名是否正確的。相反的,isGoodUser() 可能有很多用途:用來驗(yàn)證賬號是否是激活的,用來驗(yàn)證用戶名或者密碼是否正確,或者是用來搞清楚用戶是不是個(gè)好人。方法名表意不清,這就很難讓開發(fā)者明白這個(gè)方法到底是用來干什么的。簡單的說,長一點(diǎn)并且表意清晰的方法名要比又短又表意不明的方法名好。
表意清晰的長名字會幫助開發(fā)團(tuán)隊(duì)快速的理解他們軟件的功能意圖。更大的優(yōu)點(diǎn)在于,給測試方法也起個(gè)好名字會讓軟件現(xiàn)有的要求更加的清晰。例如,本軟件要求驗(yàn)證請求的用戶名和用戶密碼是不同的。使用名為 testRequestedPasswordIsNotValidBecauseItMustBeDifferentThanTheUsername() 的方法清晰的表達(dá)出了方法的意圖,也就是軟件要達(dá)到的要求。
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());
}
}
這個(gè)方法簡單的被命名為testRequestedPasswordIsNotValid(),或者更糟的是testBadPassword()。這兩個(gè)名字都讓人很難搞清楚這個(gè)方法是用來測試什么的。不清楚或者是模棱兩可的測試方法名會帶來生產(chǎn)力的損失。從而導(dǎo)致花費(fèi)來越多的時(shí)間來理解測試,不必要的重復(fù)測試,或者是破壞了被測試的類。
最后,明了的方法名還能減少文檔和注釋的工作量。
習(xí)慣三:一個(gè)對象只進(jìn)行一類服務(wù)。
第三個(gè)喜歡就是對象只關(guān)心處理一小類獨(dú)立的服務(wù)。一個(gè)對象只處理一小部分事情將使得代碼更好讀好用,因?yàn)槊總€(gè)對象代碼量很少。更糟糕的是,重復(fù)的邏輯將花費(fèi)很多時(shí)間和成本去維護(hù)。設(shè)想一下,業(yè)務(wù)部門將來要求升級一下isRequestedPasswordValid()里的邏輯,然而有兩個(gè)不同的對象卻有著功能完全一樣但是名字不一樣的方法。這種情況下,開發(fā)團(tuán)隊(duì)要花費(fèi)更多的時(shí)間去更新兩個(gè)對象,而不是一個(gè)。
這個(gè)案例表明了 CustomerAccount類的目的就是管理一個(gè)客戶的帳號。它首先創(chuàng)建了一個(gè)帳號,然后嚴(yán)整這個(gè)帳號能否用來購買產(chǎn)品。假設(shè)軟件要給所有購買過10件物品以上的客戶打折。再創(chuàng)建一個(gè)接口叫ICustomerTransactions和一個(gè)叫CustomerTransactions的類,這樣會讓代碼更加易懂,并且實(shí)現(xiàn)目標(biāo)。

public interface ICustomerTransactions
{
//State-changing methods
public void createPurchaseRecordForProduct(Long productId)
throws CustomerTransactionsSystemException;
public void loadAllPurchaseRecords()
throws CustomerTransactionsSystemException;
//Behavior method
public void isCustomerEligibleForDiscount();
}
這個(gè)新的類里面有狀態(tài)變更和行為方法,可以儲存客戶的交易并且判斷是否能夠打折。這個(gè)類創(chuàng)建起來十分簡單,方便測試以及穩(wěn)定,因?yàn)樗鼘W⑿倪@一個(gè)目標(biāo)。一個(gè)低效率的方法是如同下面的例子一樣在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();
}

就像是上面所看到的一樣,這樣使得類具有太多職責(zé),難以讀懂,甚至更容被易誤解。代碼被誤解的后果就是降低生產(chǎn)力,費(fèi)時(shí)費(fèi)力。總的來說,最好讓一個(gè)對象和它的方法集中處理一個(gè)小的工作單元。
習(xí)慣四:狀態(tài)變更方法少含有行為邏輯
第四個(gè)習(xí)慣是讓狀態(tài)變更方法少含有行為邏輯?;旌狭藸顟B(tài)變更邏輯和行為邏輯的代碼讓人很難理解,因?yàn)樵谝粋€(gè)地方處理了太多的事情。狀態(tài)變更方法涉及到遠(yuǎn)程調(diào)用來存儲數(shù)據(jù)的話很容易產(chǎn)生系統(tǒng)問題。如果遠(yuǎn)程方法是相對獨(dú)立的,并且方法本身沒有行為邏輯,這樣診斷起狀態(tài)改變方法就會十分容易。另外一個(gè)問題是,混合了行為邏輯的狀態(tài)代碼很難進(jìn)行單元測試。例如,getPostLogonMessage() 是一個(gè)依靠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()是一個(gè)使用遠(yuǎn)程調(diào)用來加載 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()方法就行了。每個(gè)場景都可以在使用遠(yuǎn)程調(diào)用連接數(shù)據(jù)庫的情況下進(jìn)行測試。例如,如果 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());
}
下面這個(gè)反例將
getPostLogonMessage() 的行為邏輯和
loadAccountStatus()的狀態(tài)轉(zhuǎn)變都放到了一個(gè)方法里,我們不應(yīng)該這么做:

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


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

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;
}
然而,如果兩個(gè)方法的邏輯推理是相互獨(dú)立的,那么就可以支持第二個(gè)情景了。在下面的一個(gè)例子中,postLogonMessage是getPostLogonMessage()創(chuàng)建的一個(gè)局部變量。

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.";
}
}
讓這兩個(gè)方法之間相互獨(dú)立的另一個(gè)好處是更容易理解。例如,isActiveForPurchasing()如果只是用來回答如“能否購買”的問題會顯得可讀性更佳,如果是用來解決“顯示登陸消息”就不那么好了。另一個(gè)好處就是測試是獨(dú)立的,讓測試更加簡單和容易理解:

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