一天一點代碼壞味道(2)


作為一個后端工程師,想必在職業生涯中都寫過一些不好維護的代碼。本文是我學習《代碼之丑》的學習筆記,今天第二天,品品重復代碼和長函數方法的味道。

上一篇:一天一點代碼壞味道(1)

1 重復代碼

CVS=Ctrl C + Ctrl V + Ctrl S,沒錯,這就是我們每天在干的事情。

CVS一時爽,重復代碼少不了。

對於重復代碼最直接的建議就是DRY原則,即Don't Repeat Yourself,換成人話就是提取公共方法,然后在需要的地方,調用這個方法代替CVS。

代碼結構重復

壞味道代碼:

public void SendBook()
{
    try
    {
        _service.SendBook();
    }
    catch (SendFailureException ex)
    {
        _notification.Send(ex);
        throw ex;
    }
}

public void SendChapter()
{
    try
    {
        _service.SendChapter();
    }
    catch (SendFailureException ex)
    {
        _notification.Send(ex);
        throw ex;
    }
}

public void StartTranslation()
{
    try
    {
        _service.StartTranslation();
    }
    catch (SendFailureException ex)
    {
        _notification.Send(ex);
        throw ex;
    }
}

有經驗的童鞋應該一眼就發現了其中的變化部分與不變的部分,不變的部分就是try-catch的結構及catch中的邏輯,而變化的則是try里面的調用service的業務邏輯。

那么,針對重復代碼,最有效的就是提取方法(Extract Method)重構。

針對上面這個場景,可以提取出一個通用的方法:

public void ExecuteTask(Action action)
{
    try
    {
        action();
    }
    catch (SendFailureException ex)
    {
        _notification.Send(ex);
        throw ex;
    }
}

然后,哼哼,重構那幾個Send方法,通過復用公共方法減少重復代碼。這里使用了C#的Lambda表達式來聲明Action委托。

對Action委托不熟悉的童鞋可以讀一讀微軟的官方文檔:https://docs.microsoft.com/zh-cn/dotnet/api/system.action

public void SendBook()
{
    ExecuteTask(() => _service.SendBook());
}

public void SendChapter()
{
    ExecuteTask(() => _service.SendChapter());
}

public void StartTranslation()
{
    ExecuteTask(() => _service.StartTranslation());
}

由此,我們可以看出,例子中的重構也並不復雜,關鍵還是在於:是否能發現結構上的重復。換句話說,即我們是否有足夠的嗅覺發現代碼中的壞味道。

選擇重復

實際應用中,我們只要看到了if語句的出現,而if和else的代碼塊長得又比較相像,那么多半就是一個壞味道無疑了。

壞味道代碼:

if (_user.IsEditor())
{
   _service.EditChapter(chapterId, title, content, true);
}
else
{
   _service.EditChapter(chapterId, title, content, false);
}

可以發現,這個if和else里面的兩段代碼幾乎一致!但是,它真的是你有可能會寫出來的代碼,因為它沒有錯,只是因為你在寫的時候只想到了if之后要做什么,而沒有考慮這個if到底要干什么。

於是,重構一番:

bool approved = _user.IsEditor();
_service.EditChapter(chapterId, title, content, approved);

嗯,好看多了,滿意得去接了杯開水。

當然,如果追求更遠一點,那么可能會發現approved這個變量可能是一個變化點,未來如果判斷的條件增加了或變化了呢,至少主方法這里不用變化。

bool approved = IsApproved(_user);
_service.EditChapter(chapterId, title, content, approved);

private bool IsApproved(User user)
{
    return user.IsEditor();
}

綜述,對於重復代碼,我們要想到DRY原則,關鍵點就是能夠發現這些重復的代碼,找到變化的和不變的部分,提取新方法,復用它!

不要重復自己,不要CVS!

2 長函數方法

我們每個程序員在心中對於一個方法最多應該多少行應該都不一樣,有人認為20行,也有人認為100行。

在Bob大叔的《代碼整潔之道》中提到,一個函數的最佳閱讀體驗是保持在20行以內。鄭曄老師(《軟件設計之美》《代碼之丑》專欄作者)對自己的要求是表達能力強的動態語言如Python/Ruby,1行代碼,而表達能力弱的靜態語言如Java,則是10行代碼。熊傑(《重構》譯者)老師對Java的代碼行數要求則是7行。由此可見,越是厲害資深的程序員,對函數方法行數的要求越短小。

當然,我們可以在做Code Review的時候抽查團隊成員的函數方法的行數是否滿足了團隊所要求的最高數值,這個數值一定是和你團隊成員的平均能力值相匹配的。

那么,長函數方法到底怎么產生的呢?

一般來說,有以下幾個原因:

(1)以性能為由

雖然是個站不住的理由,但是的確有人這么說。但是,性能優化不應該是寫代碼的第一考量要素。

(2)平鋪直敘

這是一個常見的理由,因為我們稍不注意就喜歡將自己想到的一點點羅列出來,這顯然是寫代碼之前的設計工作不充分造成的。

這里我就不貼示例代碼了,我相信大家只要維護過一個老系統,應該或多或少都有遇到。

之前我的團隊里面,就有很多人都是這樣把一個方法寫到了100行+,簡直不忍直視。然后我隨機抽查Review的時候看到了,就讓他們抽空改了,越早發現,越早重構會比較好,拖到后面上線了修改成本就更高了。

(3)一次加一點

這也是一個常見的理由,大家其實都比較委屈,畢竟自己也沒做太多,只是加了一點點而已。

比如下面這個判斷條件,經歷過多次的需求之后,它就變成了這個樣子:

if (code == 400 || code == 401 || code == 402 || ...
  || code == 500 || ...
  || ...
  || code == 10000 || ...)
{
  // 做一些錯誤處理
}

嗯,每個人都沒錯,只是結果很糟糕。

綜述,長函數方法簡直令人深惡痛絕,沒有程序員願意去閱讀長函數方法,但自己又在不經意間寫出了長函數方法

自己寫的長函數,那就讓別人慢慢讀去吧!

3 小結

本文總結了兩類壞味道,一是重復代碼,二是長函數方法。對於重復代碼,我們要做的就是不要重復,爭取復用。而對於長函數方法,我們則要控制行數規模,而且越低越好。

最后,感謝鄭曄老師的這門《代碼之丑》課程,讓我受益匪淺!我也誠心把它推薦給關注Edison的各位童鞋!

參考資料

鄭曄,《代碼之丑》(推薦訂閱學習)

Martin Flower著,熊傑譯,《重構:改善既有代碼的設計》(推薦至少學習第三章)

👇掃碼訂閱《代碼之丑》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM