代碼中應該怎麼寫函數

目錄

 

函數

短小

代碼塊和縮進

只做一件事

函數中的區段

每個函數一個抽象層級

switch語句

使用描述性的名稱

函數參數

一元函數的普遍形式

標識參數

二元函數

三元函數

參數對象

參數列表

動詞和關鍵字

無副作用

輸出參數

分隔指令與詢問

使用異常替代返回錯誤碼

抽離try/catch代碼塊

錯誤處理就是一件事

依賴磁鐵

別重複自己

結構化編程


函數

短小

函數的第一規則是短小。第二條規則是更短小。程序的每個函數都應爲2-4行長。每個函數都一目瞭然。每個函數都只做一件事。而且每個函數都依序把你帶到下一個函數。

代碼塊和縮進

if、else、while語句,其中的代碼塊應該只有一行。該行應爲函數調用語句。函數具有說明性的名稱,以增加閱讀性。

只做一件事

函數應該做一件事。做好這件事,只做這一件事。

拆分方法:

  1. 一個抽象層級對應一個函數,一個函數只做一件事。
  2. 看是否能拆出一個函數,該函數不僅只是單純地詮釋其實現。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());
    }
}

此代碼的問題:

  1. 如有新的僱員,會加長。
  2. 不止做了一件事
  3. 違反了單一職責原則(SRP)
  4. 違反了開放閉合原則(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());
        }
    }
}

使用描述性的名稱

函數越短小,功能越集中,就越便於去個好名字。

  1. 不怕長名稱。長且具有描述性的名稱要比短且令人費解的名稱要好,也比描述性的長註釋要好。
  2. 不怕花時間取名字。
  3. 描述性的名稱能理清你關於模塊的設計思路,並且幫你改進。
  4. 命名方式要一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。例如:IncludeSetupAndTearDownPages、IncludeSetupPages、IncludeSuiteSetupPages。這些名稱使用了類似的措辭,依序講出一個故事。

函數參數

最理想的參數是零,其次是一,再次是二,應儘量避免三。有足夠特殊的理由才能用三個以上的參數。

一元函數的普遍形式

  1. 詢問關於那個參數的問題,如:bool FileExists("myFile");
  2. 操作參數,或者轉變爲其他東西,如 FileStream FileOpen("myFile");

標識參數

標識參數醜陋不堪。這樣做方法名稱會變得複雜起來。如果標識爲true這樣做,標識爲false那樣做。

例如,方法爲Render(bool isSuite)一分爲二:RenderForSuite()和RenderForSingleTest()。

二元函數

有意義的應用範圍:

  1. 單個值的有序組成部分。如:Point p = new Point(0,0);
  2. 使用一些機制轉爲一元函數。如:
    1. 將WriteField寫成OutputStream的成員之一:outputStream.WriteField(name)
    2. 將outputStream作爲當前類的成員變量
    3. 分離出類似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沒有壞處。

 

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