編寫自文檔化的代碼

作者: Anders Cui  來源: 博客園  發布時間: 2009-06-22 09:54  閱讀: 1985 次  推薦: 0   原文鏈接   [收藏]  
摘要:對于我們程序員來說,我們的工作也是寫作——幾乎每天都要寫代碼;而且還要載“道”,不僅僅要滿足客戶的需求,還要讓代碼具有高度的可讀性,這樣其他的程序員可以更容易地對代碼進行修改和擴展。

文所以載道也。  —— 宋·周敦頤《通書·文辭》

對于我們程序員來說,我們的工作也是寫作——幾乎每天都要寫代碼;而且還要載“道”,不僅僅要滿足客戶的需求,還要讓代碼具有高度的可讀性,這樣其他的程序員可以更容易地對代碼進行修改和擴展。

按這樣的要求,我們需要為代碼編寫足夠的文檔,也就是將代碼“文檔化”。常見的做法有兩種,外部文檔和注釋。

外部文檔

外部文檔指的是在代碼文件之外編寫的附加文檔,比如在Word文檔中采用大量的篇幅(如UML圖、表格)來設計或記錄相關的包、類型、類型成員、成員參數之類的信息。這看起來很規范,但如果你用過這種方式,一定會討厭它。這種方式的主要問題在于:

1)增加很多額外的工作:編寫代碼本身的壓力已經很大,在壓力之下,我們往往選擇做那些最必需的事情,就是實現功能,如果時間緊急,編寫文檔就可能是草草了事了。

2)文檔需要與代碼保持同步:即使你在開始認真編寫了文檔,后來代碼有了修改和擴展(這是不可避免的,即使采用所謂的凍結需求),那么文檔就需要更新,否則就會提供誤導信息。

3)大量的文檔難以管理:如果代碼量較大,那么本身就需要大量的文檔;同時文檔也需要進行版本管理,那么就產生了不同版本的文檔;另外這些文檔基本上是一些簡單的文本。如此一來,要在這些文檔中找到所需的信息,難上加難。

文檔具有這些問題,一個重要的原因是,它們離代碼太“”了。我們可以將它們搬到代碼文件里面,這就是第二種做法:注釋。

注釋

程序員對文檔往往比較抵觸,對注釋的態度就溫和多了,甚至相當一部分人支持編寫大量的注釋。的確,如果在IDE中看到分布合理的綠色代碼塊(注釋文本的常見顏色),人們會感覺比較舒服,如果滿屏幕全是代碼,心里不免會犯怵。

從語法的角度來看,注釋就是編譯器將忽略不計的源代碼塊。所以,在這里你想寫什么就寫什么。

從語義的角度來看,注釋是昏暗泥濘的小路和明亮通暢的大道之間的區別。注釋是對其所處位置的代碼的解釋,它可以強調某些特定問題、描述某個復雜算法、對代碼進行合理分隔、協助進行維護的程序員(這個人有可能是你自己)。由此可把注釋看作是代碼的一種“內部文檔”。

那是不是就需要大量的注釋呢?至少我們曾經被這樣教導過,但事實并非如此。不知道你的習慣怎樣,我在閱讀代碼的時候,看到注釋一般會先看注釋,我在假定這些注釋對代碼提供了附加的價值。但我發現,注釋往往很隨意,甚至有可能誤導別人。你可以說,這是注釋編寫者的問題,注釋是無辜的,但必須承認的是,注釋比代碼更容易說謊。究其原因,注釋雖然離代碼很近,但仍然是一種文檔,它具有與外部文檔類似的問題:

1)增加很多額外的工作

2)需要與代碼保持同步

3)大量的注釋可能會妨礙代碼的閱讀:注釋在那里,人們不能置之不理,如果注釋太多就成阻礙了。《重構》一書認為注釋過多是一種“壞味道”。

看來,注釋也有不少問題,它們離代碼仍有距離。能否將“文檔”與代碼的距離再拉近一點?那該怎么做?

先來考慮我們為什么要添加注釋。這往往是因為代碼本身不容易讓人看懂,也就是說代碼的意圖和表現有距離,所以才需要使用注釋。如果能夠做到讓代碼本身就體現出意圖,是不是就不需要注釋了?這種方式就是本文的主題:代碼的自文檔化。(需要注意的是,自文檔化能夠取代大多數的注釋,但并不能100%取代)

什么是代碼的自文檔化(Self Documenting Code)?

唯一能完整并正確地描述代碼的文檔時代碼本身,通常情況下,這也是你能獲得的唯一文檔。因此,我們應當努力使代碼成為良好的文檔,一種人人可以讀懂的文檔。這也就是通常所說的良好的可讀性,做到了這一點,犯錯的可能性就降低了,同時代碼的維護成本也降低了——人們不需要花太多時間去熟悉你的代碼。(更多信息,可以參考Ward Cunningham的wiki

代碼自文檔化的技巧

我們可以采用多種方式來提高代碼的可讀性。其中一些技巧是非常基礎的,我們在編程之初已學習過,而有些則更為巧妙。這里首先給出兩個例子,加深一下你對代碼可讀性好壞的印象。

讓人郁悶的代碼
static int fval(int i)
{
    
int ret = 2;
    
for (int n1 = 1, n2 = 1, i2 = i - 3; i2 >= 0--i2)
    {
        n1 
= n2; n2 = ret; ret = n1 + n2;
    }
    
return (i < 2? 1 : ret;
}

 

讓人舒服的代碼


看到第一個函數,是不是想罵人了?罵完了,你還得跟蹤代碼才能了解代碼在干什么;而第二個則很清晰,你會由衷的感謝代碼的作者。事實上,兩者的功能一樣,都是計算斐波納契序列的值。所以說,為了不被人罵,我們也得注重代碼的可讀性:) 下面來看看有哪些具體的技巧。

1)好的代碼樣式

代碼的樣式極大地影響著代碼的清晰度。

a)讓“正常”流程貫穿你的代碼,異常情況不該擾亂它。比如在使用if…else…時。

b)避免過多的嵌套

c)還有其它更為基本的,比如代碼行不要過長,一行內只有一個語句等等(見前面的壞例子)。

2)選擇有意義的名稱

這一點怎么強調都不過分。如果文件、類型、函數、變量、參數都有了適當的名稱,那么將減少大量的注釋。這種方式可以讓我們編寫更接近于自然語言的代碼。

關于有意義的名稱,可以寫另一篇文章了。這里僅作簡單介紹。

a)屬性、變量和參數命名

如果是名詞性的內容,用名詞命名。如user,userName(考慮一下兩者的不同),還有elapsedTime等等。

如果是邏輯性的內容,要體現出來。比如isEmpty,isRunning等等。對比下面的代碼:

使用良好的命名
if(isEmpty)
{
}


if(flag)
{
}

b)函數/方法命名

函數往往表示行為,所以在邏輯上應包含一個動詞,如Reset、Start等等。

有時我們還可以隱含參數和返回值的信息,如看到ContainsKey,我們可以了解它的參數應當是key,而返回值為bool類型的值,表示是否包含該key。

這樣的例子,從.NET Framework的類庫中可以大量看到,要學習這些規范,建議閱讀《.NET設計規范》。

回頭看看那個可惡的fval(),你能看出它是干什么的嗎?

c)類型和命名空間的命名

這些也有一些約定俗成的東西,比如Attribute、Exception和接口命名等等,最重要的是團隊內保持一致。在此不再贅述。

d)文件的命名

一般而言,文件的名字應該能反映出所包含的類型。但在.NET中,有partial類的概念,同一個類的代碼可能分布在不同文件中,這時建議先對代碼進行合理的分組,其中一個主文件,它的名稱與類型名稱相同,其它文件以此為基礎,并且整個團隊保持一致。比如ASP.NET中的Default.aspx.cs和Default.aspx.designer.cs。

關于命名,還有一個要注意的地方:并非寫得越多越好。比如PersonClass,DataObject,還有常見的匈牙利命名法,iCount,strName這樣的都該放棄,別人從count和name就可以了解到足夠的信息了。

3)分解為原子函數

重構》一書曾提到,“當我看到一段需要注釋才能讓人理解意圖的代碼,我就會將它放進一個獨立的函數中”。這種方式衍生的一個問題是,函數細化到什么程度?

a)一個函數,一種操作:同時為此操作起一個容易理解的名字,這時,注釋還需要嗎?

b)減少副作用:副作用總需要額外的說明

c)簡短:雖然你是想只包含一種操作,但發現最后的代碼竟然有數百行,那說明還不夠細化,繼續分解,如此遞歸下去

4)通過語言層面的機制減少誤解

a)如果參數是非負整數,那么考慮用uint來代替int,否則就需要添加注釋來說明

b)如果需要一個不能被改變的值,就使用readonly或const

c)使用枚舉描述一組相關的值

d)選擇合適的修飾符,當你為方法使用的修飾符是public或private時,別人讀到時的感受肯定不同

5)使用常量

在閱讀代碼的時候,映入眼簾的如果是這樣的代碼:if(counter == 76),相信你會變得緊張、不知所措,這樣的數字稱為魔數(Magic Number)。我們的程序不需要像魔術那樣讓人看不懂,我們現在討論的是提高可讀性。為它添加一個有意義的名字吧,比如bananasPerCake,它的一個額外好處是帶來了更好的可維護性,因為現在只要修改一處。

這里針對不僅僅是數字,而是任何讓人看不懂的硬編碼。

6)強調重要的代碼

有時代碼的讀者不需要了解所有信息,這時可以:

a)在類中按一定的順序進行聲明。比如public的成員放在前面,這往往是讀者最需要了解的,可以考慮的一種方式是使用C#中的#region。

b)隱藏不重要的信息,這時自然就強調了重要的信息了

c)限制嵌套的條件語句的數量,否則就容易掩蓋那些重要的條件分支了

7)考慮上下文信息

在一個地方就只考慮它該考慮的事情。比如異常處理,在團隊內可以約定,統一在業務邏輯層進行處理,而不是在任何地方都進行處理。

關于相關的技巧就寫到這里,相信還會第8, 9, …, N條可以補充,不過在這些之后如果還是不滿足怎么辦?這時候考慮使用注釋。

N+1)編寫有意義的注釋

如果不能以代碼本身的方式提高可讀性,最后才考慮注釋。關于編寫注釋,又可以寫一篇文章了,這里只想說,把注釋作為最后一種選擇,而且添加的注釋應該給代碼帶來附加價值

不過,我們總有犯錯誤的時候,怎樣補救呢?重構代碼吧,為了自己,也為了其他要閱讀你代碼的人。不過,可以使用一些好用的工具,免得在重構的時候繼續犯錯,這里推薦一個免費的工具:CodeRush Xpress,我曾做過一個介紹。它極大地增強了VS的重構能力,對于上面提供的幾點比如代碼樣式、重命名、提取方法、提取常量都提供了良好的支持。

最后,介紹一下我最近在考慮的一種編碼方式,希望它能幫助你編寫更具可讀性的代碼。

前面說了這么多,其目的就是讓代碼具有更好的可讀性,那最有可讀性的文本是什么呢?當然是自然語言,但我們不能以自然語言編碼(至少現在還不行)。那么退一步,考慮偽代碼,它介于自然語言和編程語言之間,形式非常靈活。如果能以偽代碼的方式編碼,是不是也很好?這里還是借助于CodeRush Xpress。

以接近偽代碼的方式編程

以我最近遇到的一個問題為例,不過要簡化一下。客戶需要以編程的方式從VSS上Checkout一個文件,現在了解到實現此功能需要的信息有文件的服務器端路徑,本地路徑,本次操作的注釋信息,還有如果本地文件如果是可寫的,要提示用戶是否覆蓋,暫時先考慮這些。首先對于輸入的服務器端路徑可能對應文件夾,也可能對應文件,要分開考慮,此時可以寫出如下的代碼:

C# Code
public void Checkout()
{
    
string serverPath;
    
string localPath;
    
string comment;

    
if (IsFile(serverPath))
    {
        CheckoutFile(serverPath, localPath, comment);
    }
    
else
    {
        CheckoutFolder(serverPath, localPath, comment);
    }
}


這樣需要三個新方法:IsFile、CheckoutFile和CheckoutFolder,注意它們的命名表達了我的思路(意圖)。在VS中可以生成它們的方法存根:

generate-method-stub

IsFile比較簡單,先不管它,現在考慮CheckoutFile的實現。在Checkout一個文件時,要考慮本地文件可寫的情況,可以寫出如下的代碼:

C# Code
private void CheckoutFile(string serverPath, string localPath, string comment)
{
    
if (IsWritable(localPath))
    {
        
if (!WantToGetLocalCopy())
        {
            CheckoutWithoutGettingLocalCopy(serverPath, localPath, comment);
        }
    }

    CheckoutAndGetLocalCopy(
serverPath, localPath, comment);
}


這里又添加了幾個方法,方法名同樣表達了思路。到這一步方法們都比較小了,可以直接實現,對于CheckoutFolder,也按同樣方式實現。最后檢查一下,開始聲明的幾個局部變量應該作為參數,把它們“提升”為參數:

promote-to-parameter

這樣完成了Checkout方法。這種方式的好處在于基本上按照自然的思維方式來編碼,把想做的事情命名為方法即可,可讀性自然較高。而且,最終將方法分解、細化,符合前面的第三條。建議嘗試一下這種方法。

小結

文檔的可讀性是如此重要,以至于可以作為程序員是否職業的一個標準。通過本文的分析可以了解到,使代碼自文檔化是提高可讀性的最佳選擇。

0
0
 
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()