目錄
函數
短小
函數的第一規則是短小。第二條規則是更短小。程序的每個函數都應爲2-4行長。每個函數都一目瞭然。每個函數都只做一件事。而且每個函數都依序把你帶到下一個函數。
代碼塊和縮進
if、else、while語句,其中的代碼塊應該只有一行。該行應爲函數調用語句。函數具有說明性的名稱,以增加閱讀性。
只做一件事
函數應該做一件事。做好這件事,只做這一件事。
拆分方法:
- 一個抽象層級對應一個函數,一個函數只做一件事。
- 看是否能拆出一個函數,該函數不僅只是單純地詮釋其實現。G34123???????????
函數中的區段
只做一件事的函數無法被合理地且分爲多個區段。
每個函數一個抽象層級
自頂向下讀代碼:向下規則。——讓每個函數後面都跟着位於下一抽象層級的函數。這樣一來,在查看函數列表時,就能循抽象層級向下閱讀了。
switch語句
避開switch語句是不可能的,不過還是能夠確保每個switch都埋藏在較低的抽象層級,而且永遠不重複——利用多態來實現這一點。
整理前的代碼
public Money CalculatePay(Employee e)
{
switch (e.GetType())
{
case COMMISSIONED:
return CalculateCommissionedPay(e);
case HOURLY:
return CalculateHourlyPay(e);
case SALARIED:
return CalculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.GetType());
}
}
此代碼的問題:
- 如有新的僱員,會加長。
- 不止做了一件事
- 違反了單一職責原則(SRP)
- 違反了開放閉合原則(OCP)
使用多態的代碼
public abstract class Employee
{
public abstract bool IsPayDay();
public abstract Money CaculatePay();
public abstract void DeliverPay(Money pay);
}
public interface IEmployeeFactory
{
Employee MakeEmployee(EmployeeRecord r);
}
public class EmployeeFactory : IEmployeeFactory
{
public Employee MakeEmployee(EmployeeRecord r)
{
switch (r.GetType())
{
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return HourlyEmployee(r);
case SALARIED:
return SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.GetType());
}
}
}
使用描述性的名稱
函數越短小,功能越集中,就越便於去個好名字。
- 不怕長名稱。長且具有描述性的名稱要比短且令人費解的名稱要好,也比描述性的長註釋要好。
- 不怕花時間取名字。
- 描述性的名稱能理清你關於模塊的設計思路,並且幫你改進。
- 命名方式要一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。例如:IncludeSetupAndTearDownPages、IncludeSetupPages、IncludeSuiteSetupPages。這些名稱使用了類似的措辭,依序講出一個故事。
函數參數
最理想的參數是零,其次是一,再次是二,應儘量避免三。有足夠特殊的理由才能用三個以上的參數。
一元函數的普遍形式
- 詢問關於那個參數的問題,如:bool FileExists("myFile");
- 操作參數,或者轉變爲其他東西,如 FileStream FileOpen("myFile");
標識參數
標識參數醜陋不堪。這樣做方法名稱會變得複雜起來。如果標識爲true這樣做,標識爲false那樣做。
例如,方法爲Render(bool isSuite)一分爲二:RenderForSuite()和RenderForSingleTest()。
二元函數
有意義的應用範圍:
- 單個值的有序組成部分。如:Point p = new Point(0,0);
- 使用一些機制轉爲一元函數。如:
- 將WriteField寫成OutputStream的成員之一:outputStream.WriteField(name)
- 將outputStream作爲當前類的成員變量
- 分離出類似FieldWriter的新類,在其構造函數中調用outputStream,並且包含一個Write方法。
三元函數
三元函數會讓人蔘數理解不全,儘量不要使用。
參數對象
可以將參數對象封裝爲參數對象,以達到減少參數的目的。如
Circle makeCircle(double x,double y,double radius);
Circle makeCircle(Point center,double radius);
參數列表
可變參數雖說是可變的,但認爲是單參(params)。
動詞和關鍵字
對於一元函數,函數和參數應當形成動詞/名詞對形式,如:WriteField(name),它告訴我們name是一個Field。
無副作用
public bool CheckPassword(string username, string password)
{
User user = UserService.FindByName(username);
if (user != null)
{
string codePhrase = user.GetPhraseEncodeByPassword();
string phrase = cryptographer.Decrypt(codePhrase, password);
if (phrase == "Valid Password")
{
Session.Initialize();
return true;
}
}
return false;
}
副作用在於Session.Initialize(),CheckPassword沒有暗示需要初始化Session,語義不明確。應將CheckPassword改爲CheckPasswordAndInitializeSession(),雖然還是違反了"只做一件事"的規則。
輸出參數
如:appendFooter(str); 這個方法不清楚其意義,str是輸出函數還是輸入函數?最好是這樣調用report.appendFooter(str);
分隔指令與詢問
指令與詢問合在一起:
public bool Set(string attribute,string value);
if(Set("username","unclebob"))
{
...
}
Set方法本意爲:如果"username"屬性值之前已被設置爲"unclebob"。
指令與詢問拆分開:
if(AttributeExists("username"))
{
SetAttribute("username","unclebob")
}
使用異常替代返回錯誤碼
if (DeletePage(page) == E_OK)
{
if (registry.DeleteReference(page.Name) == E_OK)
{
if (configKeys.DeleteKey(page.Name.MakeKey()) == E_OK)
{
logger.Log("page deleted");
}
else
{
logger.Log("configKeys not deleted");
}
}
else
{
logger.Log("DeleteReference from registry faild");
}
}
else
{
logger.Log("deleteed faild");
return E_ERROE;
}
使用異常替代返回錯誤碼,將錯誤代碼從主路徑中分離出來。
try
{
DeletePage(page);
registry.DeleteReference(page.Name);
configKeys.DeleteKey(page.Name.MakeKey());
}catch(Exception e)
{
logger.Log(e);
}
抽離try/catch代碼塊
最好將try和catch代碼主體部分抽離出來,另外形成函數。
public Delete(Page page)
{
try
{
DeletePageAndAllRefences(page);
}catch(Exception e)
{
LogError(e);
}
}
private void DeletePageAndAllRefences(Page page)
{
DeletePage(page);
registry.DeleteReference(page.Name);
configKeys.DeleteKey(page.Name.MakeKey());
}
private void LogError(Exception e)
{
logger.Log(e);
}
錯誤處理就是一件事
函數應該只做一件事。錯誤處理就是一件事。因此,處理錯誤的函數不該做其他事。這意味着如果關鍵字try在某個函數中存在,它就該是這個函數的第一個單詞,而且在catch/finally代碼塊後面也不該有其他內容。
依賴磁鐵
返回錯誤碼通常暗示某處有個類或是枚舉,定義了所有錯誤碼。
public enum Error
{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT
}
這樣的類就是一塊依賴磁鐵(dependency magnet);其他許多類都得導入和使用它。當Error枚舉修改時,所有這些其他的類都需要重新編譯和部署。這對Error類造成了負面壓力。程序員不願增加新的錯誤代碼,因爲這樣他們就得重新構建和部署所有東西。於是他們就複用舊的錯誤碼,而不添加新的。
使用異常替代錯誤碼,新異常就可以從異常類派生出來,無需重新編譯或重新部署。(也是OCP的一個範例)
別重複自己
當你重複了4次,當算法改變是需要修改4處。而且也會增加4次放過錯誤的可能性。
結構化編程
結構化編程認爲:每個函數、函數中的每個代碼塊應該只有一個入口、一個出口(單入單出原則)。意味只能有一個return語句,不能有break、coutinue,永遠不能有goto。——應用在大函數中。
只要函數保持短小,那麼偶爾出現的return、break、coutinue沒有壞處。