標題: 編寫易于理解代碼的六種方式 [打印本頁]

作者: 51黑黑黑    時間: 2016-2-22 23:02
標題: 編寫易于理解代碼的六種方式
對于一名開發(fā)人員,時間是最寶貴的資源。本文所要介紹的這六種編寫可維護代碼的方法可以保證讓您節(jié)省時間和少受挫折:在編寫注釋上多花一分鐘,會讓您少受一小時研讀代碼的痛苦折磨。
我學習編寫、改善和維護代碼的過程是很艱苦的。在過去的 12 年里,我一直在編寫計算機游戲并通過曾紅極一時的共享軟件技術進行網(wǎng)絡銷售,并以此為生。這就是說,我常常要從空白的屏幕開始從頭編碼,當代碼達到數(shù)萬行之后才能拿去銷售。

    這也就是說,如果我出了錯,我必須要自己去解決問題。當我在凌晨三點還在竭力尋找 bug 的時候,看著這些不知所云的晦澀代碼,我不禁自問:“我的天啊,這些垃圾代碼究竟是哪個笨家伙寫的?”,很不幸,問題的答案是 “我”。

    在學習了良好、正規(guī)的編碼技巧之后,我大受其益。本文就包括了其中的一些實踐。具有豐富經(jīng)驗的資深程序員大都對這些內容爛熟于心。他們可以通過本文優(yōu)美的散文式的介紹再重溫一遍,并可回顧一下在采取清晰編碼的理念之前,編碼是多么地令人頭痛。

    但更多的人會如同我一樣,是無意間跌跌絆絆地闖入編程領域的,而且沒有人為其灌輸這些編程技巧和理念。本文所要介紹的這些內容對很多人來說也許很基礎,但對于其他人來說卻是極為寶貴的資源,因為之前沒有人告訴過他。所以,如果您不想走彎路,那么本文將非常適合您。

    為了便于解釋,本文全篇都將使用一個示例太空游戲程序,稱為 Kill Bad Aliens。在這個游戲中,您將通過鍵盤來控制一個宇宙飛船,它可以在屏幕底端水平向前或向后移動,還可以向上發(fā)射子彈。

    游戲發(fā)生在稱為 Wave 的各個時間段。在每個 wave,外星人都會一個接一個地出現(xiàn)在屏幕頂端。它們到處飛,還會投擲炸彈。外星人將按固定時間間隔出現(xiàn)。在殺死一定數(shù)量的外星人之后,一個 Wave 就告結束。

    殺死一個外星人會給您加分。當結束一個 wave 時,根據(jù)您完成游戲所需時間的長短還會有額外的分數(shù)獎勵。

    如果被炸彈擊中,您的當前飛船就會炸毀,另一個飛船繼而出現(xiàn)。如果被炸毀超過三次以上,游戲就結束了。如果您的得分很高,就會被晉級為 “人”,如果分數(shù)很低,就不能。

    現(xiàn)在,我們可以坐下來開始用 C++ 編寫這個 Kill Bad Alients 游戲了。首先定義幾個對象來分別代表飛船、玩家的子彈、敵人和敵人的子彈。然后再編寫代碼來繪制這些對象。還需要編寫代碼來讓這些對象可以隨著時間的推移而到處移動。另外,也需要編寫游戲邏輯、外星人 AI 以及能感知用戶擊鍵用意的代碼等等。

    那么,我們該如何實現(xiàn)這些以便當游戲編制完畢后,代碼易懂、易維護,最起碼地,不會一團糟呢?

提示 1:經(jīng)常注釋

    請經(jīng)常為代碼添加注釋。假設您編寫了一個過程,但沒有為它做注釋,幾個月后,您再回過頭來想對它進行一些修整(您絕對會這么做),將需要花費很多時間去研讀這些代碼,原因就是因為您之前沒有做注釋。而時間是您最為寶貴的資源。丟失的時間是永遠也找不回來的。

    但注釋和其他事情一樣也是需要技巧的。只要多練習,在這方面的技能就會不斷提高。注釋有好有壞。

    最好不要將注釋寫得過長。假設為一個函數(shù)做了注釋,而這個注釋在將來可以節(jié)省您理解代碼所需的時間,比如說 10 分鐘。這很好,F(xiàn)在假設所編寫的注釋過長,您花了 5 分鐘編寫這個注釋,之后還要再花 5 分鐘讀懂這個注釋。這樣一來,實際上沒有節(jié)省任何時間。這不是一種很好的做法。

    當然,也不要將注釋寫得過短。如果在一兩頁之長的代碼中找不到任何注釋,那么這段代碼最好清晰得 “晶瑩剔透”,否則將來研讀所需的時間將會很長。

    再有,注釋的方式不能太死板。當剛剛開始編寫注釋時,人們往往會頭腦一熱,寫下這樣的注釋:

// Now we increase Number_Aliens_on_screen by one.
Number_Aliens_on_screen = Number_Aliens_on_screen + 1;



    這么明顯的東西顯然不需要注釋。如果代碼非;靵y以致于需要逐行注釋,那么更有利的方式是首先簡化代碼。在這種情況下,注釋并不能節(jié)省時間,反倒會消耗時間。因為注釋需要時間去研讀,而且它們分布于屏幕上的實際代碼中的不同位置,所以在顯示器上一次只能看少許的注釋。

此外,千萬不要這么寫注釋:

Short get_current_score()
{
    [insert a whole bunch of code here.]

    return [some value];

    // Now we're done.
}

    “We're done” 這樣的注釋有何用處呢?真是感謝您讓我知曉。注釋下面的這個大括號以及其后跟隨的大片空白難道還不足以讓我明白這是一段代碼的結束么?同樣,在返回語句之前也不需要使用類似 “Now we return a value” 這樣的注釋。

    那么,如果您正在編寫代碼,而又沒有上司或公司的規(guī)定可以做指導,這時,又該如何注釋呢?我的做法是:對于由我自己維護的代碼,我會寫一個簡介。這樣一來,當我返回一個我很久以前編寫的過程時,我就可以查看對它的解釋。一旦我了解了其工作原理之后,我就可以很容易地理解實際的編碼了。這通常會涉及:

過程/函數(shù)之前寫幾句話,說明其功能。
對傳遞給它的數(shù)值的一個描述。
如果是函數(shù),對其返回結果的一個描述。
在過程/函數(shù)內部,能將代碼分解為更短小的任務的注釋。
對于看起來有些難懂的大塊代碼,對其成因給與簡短的解釋。
總之,我們需要在開始時給出一個描述,然后再在整個代碼內部的幾個位置加以注釋。這種做法需時不多,但卻可在將來節(jié)省大量的時間。

    如下所示是另一個取自假想的 Kill Bad Alients 游戲的例子?紤]代表玩家子彈的那個對象。需要頻繁地調用函數(shù)來將其向上移動以便檢查該子彈是否會擊中任何目標。我可能會按如下所示編寫實現(xiàn)這個功能的代碼:

// This procedure moves the bullet upwards. It's called
//NUM_BULLET_MOVES_PER_SECOND times per second. It returns TRUE if the
//bullet is to be erased (because it hit a target or the top of the screen) and FALSE
//otherwise.
Boolean player_bullet::move_it()
{
    Boolean is_destroyed = FALSE;

    // Calculate the bullet's new position.

    [Small chunk of code.]

    // See if an enemy is in the new position. If so, call enemy destruction call and
    // set is_destroyed to TRUE

    [small chunk of code]

    // See if bullet hits top of screen. If so, set is_destroyed to TRUE

    [Small chunk of code.]

    // Change bullet's position.

    [Small chunk of code.]

    Return is_destroyed;
}

    如果代碼足夠清晰,如上所示的注釋應該就已經(jīng)足夠。對像我這樣需要不時地返回這個函數(shù)來修復錯誤的人來說,這將能夠節(jié)省大量時間。


提示 2:大量使用 #define。沒錯,是要大量使用。

    假設,在我們這個假想的游戲中,希望玩家在射中一個外星人時即可獲得 10 分。有兩種方法可以實現(xiàn)這個目的。如下所示的是其中一個比較糟糕的做法:

// We shot an alien.
Give_player_some_points(10);

    This is the good way: In some global file, do this:

#define        POINT_VALUE_FOR_ALIEN    10



之后,當我們需要給出一些分數(shù)時,我們很自然地會這么寫:

// We shot an alien.
Give_player_some_points(POINT_VALUE_FOR_ALIEN);

    在某種程度上,大多數(shù)程序員都知道該這么做,但是需要遵守一定之規(guī),才能將其做好。比如,每次在定義常數(shù)時,都需要考慮在某個中心位置對其進行定義。假設,要將玩游戲的區(qū)域設置成 800 * 600 像素,請務必這么做:

#define PIXEL_WIDTH_OF_PLAY_AREA    800
#define PIXEL_HEIGHT_OF_PLAY_AREA    600

    如果,在某個日期,又想更改游戲窗口的大小了(您很可能需要這么做),若在此處就能更改數(shù)值將會節(jié)省您雙倍的時間。這是因為:第一,無需在全部代碼中查找所有提到游戲窗口是 800 像素寬的地方(800!我當時是怎么想的?)第二,無需總要修復那些由于漏掉了引用而引起的無法避免的 bug。

    當我制作 Kill Bad Aliens 游戲時,我要決定需要殺掉多少外星人一個 wave 才算結束、屏幕上一次能有多少外星人、這些外星人又以多快的速度出現(xiàn)。例如,如果我想讓每個 wave 中的外星人的人數(shù)相同,并且他們都以相同的速度出現(xiàn),我可能會編寫如下所示的代碼:

#define        NUM_Aliens_TO_KILL_TO_END_WAVE    20
#define        MAX_Aliens_ON_SCREEN_AT_ONCE        5
#define        SECONDS_BETWEEN_NEW_Aliens_APPEARING    3

    這段代碼很清晰。此后,若我覺得這個 wave 太短或外星人相繼出現(xiàn)的時間間隔過短,我就可以立即調整相應的值并立即讓游戲重新生效。

    如此設置游戲值的一個妙處是能快速地做出更改,這種立竿見影的施控感覺實在是很好。比如,如果將上述代碼改寫成如下所示:

#define        NUM_Aliens_TO_KILL_TO_END_WAVE    20
#define        MAX_Aliens_ON_SCREEN_AT_ONCE        100
#define        SECONDS_BETWEEN_NEW_Aliens_APPEARING    1

    那么,您就無法享受上述的快感和興奮了。

    順便說一下,您可能已經(jīng)注意到,我沒有為上述值做任何注釋,這是因為從變量名可以很明顯地看出這些值的意義。這正是接下來我要討論的內容。

提示 3:不要使用弄巧成拙的變量名。

    總的目標很簡單:編寫代碼以便讓那些不知道其用意的人能讀懂,讓知道其用意的人能盡快地理解。

    實現(xiàn)這一目標最好的策略是為變量、過程等賦以含義鮮明的名字。當他人看到這個變量名時,就會立刻清楚其意義,您也不必搜索整個程序來尋找 incremeter_side_blerfm 的用意何在,這大約會節(jié)省五分鐘左右的時間。

    這里需要進行一些均衡。所給出的命名應該盡量長且足夠清晰以便您能理解其含義,但也不能過長或太過怪異,如果這樣,代碼的可讀性就會受到影響。

    例如,在實際中,我可能不會像上一節(jié)所示的那樣給常量命名。我之前之所以這么做是為了讓讀者在沒有任何上下文的情況下也能充分理解這些常量的含義。在程序本身的上下文中,與如下所示的相比:

#define        MAX_Aliens_ON_SCREEN_AT_ONCE        5

我會毫不猶豫地這樣編碼:

#define        MAX_NUM_Aliens        5

    這個簡短的名字所引起的疑惑很快就會迎刃而解,而簡短的命名還會增加代碼的可讀性。

    現(xiàn)在來看看在本文中我經(jīng)常要調用的那個用來將外星人在屏幕上到處移動的代碼片段,我會毫不猶豫地這樣編碼:

// move all the Aliens
for (short i  = 0; I < MAX_NUM_Aliens; i++)
    if (Aliens[i].exists()) // this alien currently exist?
        Aliens[i].move_it();

    請注意,包含所有外星人的這個數(shù)組的名稱很簡單,叫做 Aliens。這很棒。它恰好就是我想要的那種描述性名稱,這個名稱又很簡短,即使鍵入千遍之多,我也不會感到煩悶。此數(shù)組將會經(jīng)常用到。如果將其命名為類似 all_Aliens_currently_on_screen 這樣的名稱,那么所編寫的最終代碼將會長出很多,而且代碼還會因此變得不怎么清晰。

    同樣,我還將循環(huán)變量直接命名為 i,無任何額外的說明。若是初次接觸描述性變量名這個概念,您很可能會忍不住將此循環(huán)變量命名為 "counter" 之類的名字。實際上,沒有必要這么做。命名變量的意義在于讓讀者能夠立即理解該變量的用意。人人都知道 "i"、"j" 這類名稱常常用于循環(huán)變量,所以將循環(huán)變量如此命名是完全可以的,無需多加解釋和說明。

    當然,有關變量命名還是需要多加注意。比如,有一種稱為 Hungarian Notation 的東西。其種類很多,但基本的理念是在變量名的開始添加一個標記以表示其類型(例如,所有無符號長型變量都以 ul 開頭)。這比我希望的要多少麻煩一些,但這個概念必須要了解。為了弄清楚事情可能需要花費太多時間,但還是值得的。


提示 4:進行錯誤檢查。

    一個正常大小的程序往往都會有大量的函數(shù)和過程。而且更為麻煩的是,其中的每一個都需要進行稍許錯誤檢查。

    當創(chuàng)建過程/函數(shù)時,應該總要考慮這樣的一個問題:“假如一些懷有惡意的人故意向函數(shù)或過程傳遞進各種怪異的值,這段剛剛創(chuàng)建的代碼如何能自保并且讓計算機也能免受破壞呢?”然后,編寫代碼來檢查這些惡意數(shù)據(jù)以保護自身免受這些數(shù)據(jù)的破壞。

    舉個例子。我們的這個太空游戲的主要目標是殺掉外星人并積分,所以我們需要一個過程來更改分數(shù)。而且,當加分時,我們需要調用一個例程來實現(xiàn)分數(shù)上星光閃爍的效果。如下所示的是第一個過程:

Void change_score(short num_points)
{
    score += num_points;
         make_sparkles_on_score();
}

    到目前為止還不錯,F(xiàn)在請思考一下:這里可能出現(xiàn)的錯誤是什么呢?

    首先,一個很明顯的問題是:如果 num_points 是負值該如何呢?我們能讓玩家的分數(shù)降低么?就算我們能降低分數(shù),但在我之前給出的關于該游戲的描述中,沒有提到過失分。而且,游戲應該有趣,但失分無論如何不能算是一個有趣的事情。所以,我們將分數(shù)負值視為一個錯誤并必須要捕獲。

    上述錯誤相對容易,但這里有一個很微妙的問題(也是我在游戲中經(jīng)常要處理的)。如果 num_points 為零又會怎么樣呢?

    這是一個很似是而非的情景。還記得么,我們會在每個 wave 結束時根據(jù)玩家完成速度的快慢給一個獎勵分數(shù)。如果玩家速度極慢,我們是否應該給他一個值為零的獎勵分數(shù)呢?在凌晨三點,調用 change_score 并傳遞值 0,這完全可行。

    現(xiàn)在的問題是我們可能不想讓計分板在顯示的數(shù)值沒有變化時仍舊五顏六色地閃個不停。所以我們要先捕獲這個問題。讓我們嘗試如下代碼:

Void change_score(short num_points)
{
    if (num_points < 0)
{
// maybe some error message
        return;
}

    score += num_points;

    if (num_points > 0)
             make_sparkles_on_score();
}

    好了,情況好多了。

    請注意這是很簡單的一個函數(shù)。里面并沒有用到任何極受新手推崇的新奇指針。如果要傳遞數(shù)組或指針,那么最好小心錯誤和壞數(shù)據(jù)的出現(xiàn)。

    這樣做的好處并不僅僅限于讓程序免遭破壞。好的錯誤檢查還能讓調試更為迅速。假設,您知道寫入的數(shù)據(jù)超出了數(shù)組的范圍,為了發(fā)現(xiàn)可能出現(xiàn)的錯誤,您需要詳細檢查代碼。若所查看的這個過程中的錯誤檢查均已就緒,那么就無需花很多時間去專門通查它來尋找錯誤。

    這種做法將節(jié)省大量時間,而且還能重復。還是那句話,時間是我們所擁有的寶貴資源。


提示 5:“不成熟的優(yōu)化是麻煩的根源” —— Donald Knuth

    上述格言非我個人所造,它可以在 Wikipedia 中找到,所以必定是十分睿智的。

    除非是想找別人麻煩,否則編寫代碼的首要目標就是簡明性。簡單的代碼更易于編寫、易于日后理解,也更易于調試。

    優(yōu)化與簡明性是相悖的。但有時,卻必須要進行優(yōu)化,在游戲中尤其如此。這個問題至關重要,您可能直到用解析器實際對工作代碼進行測試時才會意識到需要進行優(yōu)化。(解析器 是一種程序,用來監(jiān)視其他程序并找出該程序使用不同的調用所花費的時間。這些都是很棒的程序。您可以找一個來試試。)

    每次當我優(yōu)化游戲時,常常都禁不住會大出所料。我十分擔心的那些代碼總是問題不大,相反,我覺得萬無一失的代碼反倒會運行得十分緩慢。由于我對運行速度的快慢并沒有什么概念,在獲得實際數(shù)據(jù)之前我所進行的優(yōu)化根本就是浪費時間。比浪費時間更糟糕的是它還讓代碼變得有些混亂。

    這個規(guī)則看來很難遵守。但,如果規(guī)則很容易,它也就稱不上規(guī)則了。好的程序員大都更痛恨將原本可以運行迅速的代碼弄得臃腫笨拙。

    但好消息是,在我不斷 “該這樣不該那樣的” 布道式的介紹中, 這是惟一的一個您可以稍微懈怠一些的地方!

    請讓自己編寫的代碼盡量整潔和有效一些吧。在后面的優(yōu)化階段,可能需要將其變得面目全非。所以如非必要,請慎重。

說到傷害,接下來,就來看看最后的這條建議。


提示 6:不要一知半解、自作聰明。

    您可能聽說過 IOCCC 吧,即 International Obfuscated C Code Contest。大家都知道,C 和 C++,不管其優(yōu)勢如何卓越,都會最終導致編寫的代碼噩夢般地復雜。這個比賽就是要通過評選出最離譜的代碼來展示簡明代碼的價值,真是別具匠心。

    讓我們來看看在您自認為具有了編程的全部知識并甘愿冒險的情況下,您能制造什么樣的麻煩。足夠的知識讓您信心百倍地將十行代碼壓縮進一行代碼內。付出的代價就是您絕對無法快速修復其中可能存在的 bug。

    這里所需吸取的教訓就是如果您所編寫的代碼要求您必須具有有關復雜優(yōu)先規(guī)則的詳細知識或讓您不得不翻看某些書的后面章節(jié)才能弄清來龍去脈,那么您在編寫這段代碼時就犯了一知半解、自作聰明的毛病了。

    每個人對代碼的復雜性都有自己的容忍程度。就我個人而言,我編寫的程序往往呈比較典型的保守風格。我個人認為,如果一段 C 代碼需要您必須知道 i++ 和 ++i 之間的差別,那么這段代碼就過于復雜了。

    您盡可以把我想象成一個循規(guī)蹈矩的人。沒錯,我的確如此。但循規(guī)蹈矩卻可以讓我花很少的時間就可以讀懂我的代碼。

結束語

    至此,您可能會想:“哇哦,真是浪費時間。您介紹的所有這些東西都是顯而易見,盡人皆知的。為何還多此一舉,寫這樣的文章呢?” 實際上,我很希望您會這么想,因為這意味著您已經(jīng)進步了,變得明智了。這很好。

    但不要錯認為所有這些內容對每個人都是不言自明的。事實并非如此。糟糕的代碼隨處可見,但實際上這些代碼本不應如此。

    如果您正在努力編寫大量代碼并想讓自己不受其所累。那么就請讓代碼盡量簡單明了一些,這樣,您就可以節(jié)省大量時間和免受很多挫折。






歡迎光臨 (http://www.torrancerestoration.com/bbs/) Powered by Discuz! X3.1