【《代碼整潔之道》精讀與演繹】之三 整潔代碼的函數書(shū)寫(xiě)準則
一、引言
本文引用地址:http://dyxdggzs.com/article/201608/296317.htm以下引言的內容,有必要伴隨這個(gè)系列的每一次更新,這次也不例外。
《代碼整潔之道》這本書(shū)提出了一個(gè)觀(guān)點(diǎn):代碼質(zhì)量與其整潔度成正比,干凈的代碼,既在質(zhì)量上可靠,也為后期維護、升級奠定了良好基礎。書(shū)中介紹的規則均來(lái)自作者多年的實(shí)踐經(jīng)驗,涵蓋從命名到重構的多個(gè)編程方面,雖為一“家”之言,然誠有可資借鑒的價(jià)值。
但我們知道,很多時(shí)候,理想很豐滿(mǎn),現實(shí)很骨感,也知道人在江湖,身不由己。因為項目的緊迫性,需求的多樣性,我們無(wú)法時(shí)時(shí)刻刻都寫(xiě)出整潔的代碼,保持自己輸出的都是高質(zhì)量、優(yōu)雅的代碼。
但若我們理解了代碼整潔之道的精髓,我們會(huì )知道怎樣讓自己的代碼更加優(yōu)雅、整潔、易讀、易擴展,知道真正整潔的代碼應該是怎么樣的,也許就會(huì )漸漸養成持續輸出整潔代碼的習慣。
而且或許你會(huì )發(fā)現,若你一直保持輸出整潔代碼的習慣,長(cháng)期來(lái)看,會(huì )讓你的整體效率和代碼質(zhì)量大大提升。
二、本文涉及知識點(diǎn)思維導圖
國際慣例,先放出這篇文章所涉及內容知識點(diǎn)的一張思維導圖,就開(kāi)始正文。大家若是疲于閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點(diǎn)的大概。
三、整潔代碼的函數書(shū)寫(xiě)準則
1 短小
函數的第一規則是要短小。第二規則還是要短小。
《代碼整潔之道》一書(shū)作者Bob大叔寫(xiě)道,“近40年來(lái),我寫(xiě)過(guò)各種長(cháng)度不同的函數。我寫(xiě)過(guò)令人憎惡的長(cháng)達3000行的厭物,也寫(xiě)過(guò)許多100行到300行的函數,還寫(xiě)過(guò)20行到30行的。經(jīng)過(guò)漫長(cháng)的試錯,經(jīng)驗告訴我,函數就該短小”。
那么函數應該有多短小合適呢?通常來(lái)說(shuō),應該短于如下這個(gè)函數:
[cpp] view plain copy print?
public static StringrenderPageWithSetupsAndTeardowns
(PageData pageData, boolean isSuite
)throws Exception
{
booleanisTestPage = pageData.hasAttribute("Test");
if(isTestPage) {
WikiPagetestPage = pageData.getWikiPage( );
StringBuffernewPageContent = new StringBuffer( );
includeSetupPages(testPage,newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage,newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
returnpageData.getHtml( );
}
而其實(shí),最好應該縮短成如下的樣子:
[csharp] view plain copy print?
public static StringrenderPageWithSetupsAndTeardowns(
PageDatapageData, boolean isSuite) throws Exception
{
if(isTestPage(pageData))
includeSetupAndTeardownPages(pageData,isSuite);
returnpageData.getHtml( );
}
總之,十行以?xún)仁钦麧嵉暮瘮当容^合適的長(cháng)度,若沒(méi)有特殊情況,我們最好將單個(gè)函數控制在十行以?xún)取?/p>
評論區有一些討論,也放到正文來(lái)吧。
“函數是否應該足夠短小,算是《代碼整潔之道》中最具爭議的議題之一。
書(shū)寫(xiě)短小函數的時(shí)候,其實(shí)我們不要忽略一點(diǎn),那就是,函數名名稱(chēng)本身就具描述性。短小的函數構成,如果要追根溯源了解內部實(shí)現,自然需要一層層找到最終的實(shí)現。但若是想大致知道這個(gè)函數到底做了什么,結合這個(gè)短小函數體內具描述性的一些函數名,應該也就一目了然了。試想,當你眼前的這個(gè)函數是幾十上百上千行的龐然大物的時(shí)候,你能做到一眼就一目了然,將其大概做了什么了然于心嗎?函數短小的一方面優(yōu)點(diǎn),在這里就體現出來(lái)了。
函數應該短小這個(gè)議題,仁者見(jiàn)仁智者見(jiàn)智,在實(shí)際編碼過(guò)程中任何人都很難做到嚴格遵守,但大的方向,若想寫(xiě)出整潔的代碼,應該去向短小的函數靠攏,對吧?”
2 單一職責
函數應該只做一件事情。只做一件事,做好這件事。
設計模式中有單一職責原則,我們可以把這條原則理解為代碼整潔之道中的函數單一職責原則。
要判斷函數是不是只做了一件事情,還有一個(gè)方法,就是看能否再拆出一個(gè)函數,該函數不僅只是單純地重新詮釋其實(shí)現。
3 命名合適且具描述性
“如果每個(gè)例程都讓你感到深合己意,那就是整潔的代碼。”要遵循這一原則,大半工作都在于為只做一件事的小函數取個(gè)好名字。函數越短小,功能越集中,就越便于取個(gè)好名字。
別害怕長(cháng)名稱(chēng)。長(cháng)而具有描述性的名稱(chēng),比短而令人費解的名稱(chēng)好。而且長(cháng)而具有描述性的名稱(chēng),比描述性的長(cháng)注釋要好。且選擇描述性的名稱(chēng)能理清你關(guān)于模塊的設計思路,并幫你改進(jìn)之。當然,如果短的名稱(chēng)已經(jīng)足夠說(shuō)明問(wèn)題,還是越短越好。
命名方式要保持一致。使用與模塊名一脈相承的短語(yǔ)、名詞和動(dòng)詞給函數命名。比如,includeSetupAndTeardownPages,includeSetupPages, includeSuiteSetupPage, and includeSetupPage等。這些名詞使用了類(lèi)似的措辭,依序講述一個(gè)故事,就是是比較推崇的命名方式了。
4 參數盡可能少
最理想的函數參數形態(tài)是零參數,其次是單參數,再次是雙參數,應盡量避免三參數及以上參數的函數,有足夠的理由才能用三個(gè)以上參數(多參數函數)。
函數參數中出現標識符參數是非常不推崇的做法。有標識符參數的函數,很有可能不止在做一件事,標示如果標識符為true將這樣做,標識符為false將那樣做。正確的做法應該將有標識符參數的函數一分為二,對標識符為true和false分別開(kāi)一個(gè)函數來(lái)處理。
5 避免重復
重復的代碼會(huì )導致模塊的臃腫,整個(gè)模塊的可讀性可能會(huì )應該重復的消除而得到提升。
其實(shí)可以這樣說(shuō),重復可能是軟件中一切邪惡的根源,許多原則與實(shí)踐規則都是為控制與消除重復而創(chuàng )建的。仔細想一想,面向對象編程是如何將代碼集中到基類(lèi),從而避免了冗余的。而面向方面編程(Aspect Oriented Programming)、面向組件編程(ComponentOriented Programming)多少也是消除重復的一種策略。這樣看來(lái),自子程序發(fā)明以來(lái),軟件開(kāi)發(fā)領(lǐng)域的所有創(chuàng )新都是在不斷嘗試從源代碼中消滅重復。
重復而啰嗦的代碼,乃萬(wàn)惡之源,我們要盡力避免。
四、范例
有必要貼出一段書(shū)中推崇的整潔代碼作為本次函數書(shū)寫(xiě)準則的范例。
[csharp] view plain copy print?
using System;
public class SetupTeardownIncluder
{
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception
{
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite)throws Exception
{
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pageData)
{
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
private String render(boolean isSuite) throws Exception
{
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception
{
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception
{
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception
{
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception
{
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception
{
include("SetUp", "-setup");
}
private void includePageContent() throws Exception
{
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Exception
{
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception
{
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception
{
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception
{
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception
{
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null)
{
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception
{
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception
{
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg)
{
newPageContent
.append("n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("n");
}
}
上面這段代碼,滿(mǎn)足了函數書(shū)寫(xiě)短小、單一職責、命名合適、參數盡可能少、不重復啰嗦這幾條準則。整潔的函數代碼大致如此。
五、小結
大師級程序員把系統當作故事來(lái)講,而不是當做程序來(lái)寫(xiě)。這是之前已經(jīng)提到過(guò)的一個(gè)觀(guān)點(diǎn)。
本文講述了如何編寫(xiě)良好函數的一些準則,如果你遵循這些準則,函數就會(huì )短小,有個(gè)好名字,而且被很好的歸置。不過(guò)永遠不要忘記,我們真正的目標在于講述系統的故事,而你編寫(xiě)的函數必須干凈利落的拼裝到一起,形成一種精確而清晰的語(yǔ)言,幫助你講故事。
程序員,其實(shí)是故事家。
六、本文涉及知識點(diǎn)提煉整理
整潔代碼的函數書(shū)寫(xiě),可以遵從如下幾個(gè)原則:
第一原則:短小。若沒(méi)有特殊情況,最好將單個(gè)函數控制在十行以?xún)取?/p>
第二原則:?jiǎn)我宦氊?。函數應該只做一件事情。只做一件事,做好這件事。
第三原則:命名合適且具描述性。長(cháng)而具有描述性的名稱(chēng),比短而令人費解的名稱(chēng)好。當然,如果短的名稱(chēng)已經(jīng)足夠說(shuō)明問(wèn)題,還是越短越好。
第四原則:參數盡可能少。最理想的函數參數形態(tài)是零參數,其次是單參數,再次是雙參數,應盡量避免三參數及以上參數的函數,有足夠的理由才能用三個(gè)以上參數。
第五原則:盡力避免重復。重復的代碼會(huì )導致模塊的臃腫,整個(gè)模塊的可讀性可能會(huì )應該重復的消除而得到提升。
本文就此結束。
下篇文章,我們將繼續《代碼整潔之道》的精讀與演繹,探討更多的內容。
Best Wish~
評論