|
對于一名開發(fā)人員,時(shí)間是最寶貴的資源。本文所要介紹的這六種編寫可維護(hù)代碼的方法可以保證讓您節(jié)省時(shí)間和少受挫折:在編寫注釋上多花一分鐘,會讓您少受一小時(shí)研讀代碼的痛苦折磨。
我學(xué)習(xí)編寫、改善和維護(hù)代碼的過程是很艱苦的。在過去的 12 年里,我一直在編寫計(jì)算機(jī)游戲并通過曾紅極一時(shí)的共享軟件技術(shù)進(jìn)行網(wǎng)絡(luò)銷售,并以此為生。這就是說,我常常要從空白的屏幕開始從頭編碼,當(dāng)代碼達(dá)到數(shù)萬行之后才能拿去銷售。
這也就是說,如果我出了錯(cuò),我必須要自己去解決問題。當(dāng)我在凌晨三點(diǎn)還在竭力尋找 bug 的時(shí)候,看著這些不知所云的晦澀代碼,我不禁自問:“我的天啊,這些垃圾代碼究竟是哪個(gè)笨家伙寫的啊?”,很不幸,問題的答案是 “我”。
在學(xué)習(xí)了良好、正規(guī)的編碼技巧之后,我大受其益。本文就包括了其中的一些實(shí)踐。具有豐富經(jīng)驗(yàn)的資深程序員大都對這些內(nèi)容爛熟于心。他們可以通過本文優(yōu)美的散文式的介紹再重溫一遍,并可回顧一下在采取清晰編碼的理念之前,編碼是多么地令人頭痛。
但更多的人會如同我一樣,是無意間跌跌絆絆地闖入編程領(lǐng)域的,而且沒有人為其灌輸這些編程技巧和理念。本文所要介紹的這些內(nèi)容對很多人來說也許很基礎(chǔ),但對于其他人來說卻是極為寶貴的資源,因?yàn)橹皼]有人告訴過他。所以,如果您不想走彎路,那么本文將非常適合您。
為了便于解釋,本文全篇都將使用一個(gè)示例太空游戲程序,稱為 Kill Bad Aliens。在這個(gè)游戲中,您將通過鍵盤來控制一個(gè)宇宙飛船,它可以在屏幕底端水平向前或向后移動,還可以向上發(fā)射子彈。
游戲發(fā)生在稱為 Wave 的各個(gè)時(shí)間段。在每個(gè) wave,外星人都會一個(gè)接一個(gè)地出現(xiàn)在屏幕頂端。它們到處飛,還會投擲炸彈。外星人將按固定時(shí)間間隔出現(xiàn)。在殺死一定數(shù)量的外星人之后,一個(gè) Wave 就告結(jié)束。
殺死一個(gè)外星人會給您加分。當(dāng)結(jié)束一個(gè) wave 時(shí),根據(jù)您完成游戲所需時(shí)間的長短還會有額外的分?jǐn)?shù)獎勵。
如果被炸彈擊中,您的當(dāng)前飛船就會炸毀,另一個(gè)飛船繼而出現(xiàn)。如果被炸毀超過三次以上,游戲就結(jié)束了。如果您的得分很高,就會被晉級為 “人”,如果分?jǐn)?shù)很低,就不能。
現(xiàn)在,我們可以坐下來開始用 C++ 編寫這個(gè) Kill Bad Alients 游戲了。首先定義幾個(gè)對象來分別代表飛船、玩家的子彈、敵人和敵人的子彈。然后再編寫代碼來繪制這些對象。還需要編寫代碼來讓這些對象可以隨著時(shí)間的推移而到處移動。另外,也需要編寫游戲邏輯、外星人 AI 以及能感知用戶擊鍵用意的代碼等等。
那么,我們該如何實(shí)現(xiàn)這些以便當(dāng)游戲編制完畢后,代碼易懂、易維護(hù),最起碼地,不會一團(tuán)糟呢?
提示 1:經(jīng)常注釋
請經(jīng)常為代碼添加注釋。假設(shè)您編寫了一個(gè)過程,但沒有為它做注釋,幾個(gè)月后,您再回過頭來想對它進(jìn)行一些修整(您絕對會這么做),將需要花費(fèi)很多時(shí)間去研讀這些代碼,原因就是因?yàn)槟皼]有做注釋。而時(shí)間是您最為寶貴的資源。丟失的時(shí)間是永遠(yuǎn)也找不回來的。
但注釋和其他事情一樣也是需要技巧的。只要多練習(xí),在這方面的技能就會不斷提高。注釋有好有壞。
最好不要將注釋寫得過長。假設(shè)為一個(gè)函數(shù)做了注釋,而這個(gè)注釋在將來可以節(jié)省您理解代碼所需的時(shí)間,比如說 10 分鐘。這很好�,F(xiàn)在假設(shè)所編寫的注釋過長,您花了 5 分鐘編寫這個(gè)注釋,之后還要再花 5 分鐘讀懂這個(gè)注釋。這樣一來,實(shí)際上沒有節(jié)省任何時(shí)間。這不是一種很好的做法。
當(dāng)然,也不要將注釋寫得過短。如果在一兩頁之長的代碼中找不到任何注釋,那么這段代碼最好清晰得 “晶瑩剔透”,否則將來研讀所需的時(shí)間將會很長。
再有,注釋的方式不能太死板。當(dāng)剛剛開始編寫注釋時(shí),人們往往會頭腦一熱,寫下這樣的注釋:
// Now we increase Number_Aliens_on_screen by one.
Number_Aliens_on_screen = Number_Aliens_on_screen + 1;
這么明顯的東西顯然不需要注釋。如果代碼非�;靵y以致于需要逐行注釋,那么更有利的方式是首先簡化代碼。在這種情況下,注釋并不能節(jié)省時(shí)間,反倒會消耗時(shí)間。因?yàn)樽⑨屝枰獣r(shí)間去研讀,而且它們分布于屏幕上的實(shí)際代碼中的不同位置,所以在顯示器上一次只能看少許的注釋。
此外,千萬不要這么寫注釋:
Short get_current_score()
{
[insert a whole bunch of code here.]
return [some value];
// Now we're done.
}
“We're done” 這樣的注釋有何用處呢?真是感謝您讓我知曉。注釋下面的這個(gè)大括號以及其后跟隨的大片空白難道還不足以讓我明白這是一段代碼的結(jié)束么?同樣,在返回語句之前也不需要使用類似 “Now we return a value” 這樣的注釋。
那么,如果您正在編寫代碼,而又沒有上司或公司的規(guī)定可以做指導(dǎo),這時(shí),又該如何注釋呢?我的做法是:對于由我自己維護(hù)的代碼,我會寫一個(gè)簡介。這樣一來,當(dāng)我返回一個(gè)我很久以前編寫的過程時(shí),我就可以查看對它的解釋。一旦我了解了其工作原理之后,我就可以很容易地理解實(shí)際的編碼了。這通常會涉及:
過程/函數(shù)之前寫幾句話,說明其功能。
對傳遞給它的數(shù)值的一個(gè)描述。
如果是函數(shù),對其返回結(jié)果的一個(gè)描述。
在過程/函數(shù)內(nèi)部,能將代碼分解為更短小的任務(wù)的注釋。
對于看起來有些難懂的大塊代碼,對其成因給與簡短的解釋。
總之,我們需要在開始時(shí)給出一個(gè)描述,然后再在整個(gè)代碼內(nèi)部的幾個(gè)位置加以注釋。這種做法需時(shí)不多,但卻可在將來節(jié)省大量的時(shí)間。
如下所示是另一個(gè)取自假想的 Kill Bad Alients 游戲的例子�?紤]代表玩家子彈的那個(gè)對象。需要頻繁地調(diào)用函數(shù)來將其向上移動以便檢查該子彈是否會擊中任何目標(biāo)。我可能會按如下所示編寫實(shí)現(xiàn)這個(gè)功能的代碼:
// 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;
}
如果代碼足夠清晰,如上所示的注釋應(yīng)該就已經(jīng)足夠。對像我這樣需要不時(shí)地返回這個(gè)函數(shù)來修復(fù)錯(cuò)誤的人來說,這將能夠節(jié)省大量時(shí)間。
提示 2:大量使用 #define。沒錯(cuò),是要大量使用。
假設(shè),在我們這個(gè)假想的游戲中,希望玩家在射中一個(gè)外星人時(shí)即可獲得 10 分。有兩種方法可以實(shí)現(xiàn)這個(gè)目的。如下所示的是其中一個(gè)比較糟糕的做法:
// 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
之后,當(dāng)我們需要給出一些分?jǐn)?shù)時(shí),我們很自然地會這么寫:
// We shot an alien.
Give_player_some_points(POINT_VALUE_FOR_ALIEN);
在某種程度上,大多數(shù)程序員都知道該這么做,但是需要遵守一定之規(guī),才能將其做好。比如,每次在定義常數(shù)時(shí),都需要考慮在某個(gè)中心位置對其進(jìn)行定義。假設(shè),要將玩游戲的區(qū)域設(shè)置成 800 * 600 像素,請務(wù)必這么做:
#define PIXEL_WIDTH_OF_PLAY_AREA 800
#define PIXEL_HEIGHT_OF_PLAY_AREA 600
如果,在某個(gè)日期,又想更改游戲窗口的大小了(您很可能需要這么做),若在此處就能更改數(shù)值將會節(jié)省您雙倍的時(shí)間。這是因?yàn)椋旱谝唬瑹o需在全部代碼中查找所有提到游戲窗口是 800 像素寬的地方(800!我當(dāng)時(shí)是怎么想的?)第二,無需總要修復(fù)那些由于漏掉了引用而引起的無法避免的 bug。
當(dāng)我制作 Kill Bad Aliens 游戲時(shí),我要決定需要?dú)⒌舳嗌偻庑侨艘粋€(gè) wave 才算結(jié)束、屏幕上一次能有多少外星人、這些外星人又以多快的速度出現(xiàn)。例如,如果我想讓每個(gè) 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
這段代碼很清晰。此后,若我覺得這個(gè) wave 太短或外星人相繼出現(xiàn)的時(shí)間間隔過短,我就可以立即調(diào)整相應(yīng)的值并立即讓游戲重新生效。
如此設(shè)置游戲值的一個(gè)妙處是能快速地做出更改,這種立竿見影的施控感覺實(shí)在是很好。比如,如果將上述代碼改寫成如下所示:
#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)注意到,我沒有為上述值做任何注釋,這是因?yàn)閺淖兞棵梢院苊黠@地看出這些值的意義。這正是接下來我要討論的內(nèi)容。
提示 3:不要使用弄巧成拙的變量名。
總的目標(biāo)很簡單:編寫代碼以便讓那些不知道其用意的人能讀懂,讓知道其用意的人能盡快地理解。
實(shí)現(xiàn)這一目標(biāo)最好的策略是為變量、過程等賦以含義鮮明的名字。當(dāng)他人看到這個(gè)變量名時(shí),就會立刻清楚其意義,您也不必搜索整個(gè)程序來尋找 incremeter_side_blerfm 的用意何在,這大約會節(jié)省五分鐘左右的時(shí)間。
這里需要進(jìn)行一些均衡。所給出的命名應(yīng)該盡量長且足夠清晰以便您能理解其含義,但也不能過長或太過怪異,如果這樣,代碼的可讀性就會受到影響。
例如,在實(shí)際中,我可能不會像上一節(jié)所示的那樣給常量命名。我之前之所以這么做是為了讓讀者在沒有任何上下文的情況下也能充分理解這些常量的含義。在程序本身的上下文中,與如下所示的相比:
#define MAX_Aliens_ON_SCREEN_AT_ONCE 5
我會毫不猶豫地這樣編碼:
#define MAX_NUM_Aliens 5
這個(gè)簡短的名字所引起的疑惑很快就會迎刃而解,而簡短的命名還會增加代碼的可讀性。
現(xiàn)在來看看在本文中我經(jīng)常要調(diào)用的那個(gè)用來將外星人在屏幕上到處移動的代碼片段,我會毫不猶豫地這樣編碼:
// 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();
請注意,包含所有外星人的這個(gè)數(shù)組的名稱很簡單,叫做 Aliens。這很棒。它恰好就是我想要的那種描述性名稱,這個(gè)名稱又很簡短,即使鍵入千遍之多,我也不會感到煩悶。此數(shù)組將會經(jīng)常用到。如果將其命名為類似 all_Aliens_currently_on_screen 這樣的名稱,那么所編寫的最終代碼將會長出很多,而且代碼還會因此變得不怎么清晰。
同樣,我還將循環(huán)變量直接命名為 i,無任何額外的說明。若是初次接觸描述性變量名這個(gè)概念,您很可能會忍不住將此循環(huán)變量命名為 "counter" 之類的名字。實(shí)際上,沒有必要這么做。命名變量的意義在于讓讀者能夠立即理解該變量的用意。人人都知道 "i"、"j" 這類名稱常常用于循環(huán)變量,所以將循環(huán)變量如此命名是完全可以的,無需多加解釋和說明。
當(dāng)然,有關(guān)變量命名還是需要多加注意。比如,有一種稱為 Hungarian Notation 的東西。其種類很多,但基本的理念是在變量名的開始添加一個(gè)標(biāo)記以表示其類型(例如,所有無符號長型變量都以 ul 開頭)。這比我希望的要多少麻煩一些,但這個(gè)概念必須要了解。為了弄清楚事情可能需要花費(fèi)太多時(shí)間,但還是值得的。
提示 4:進(jìn)行錯(cuò)誤檢查。
一個(gè)正常大小的程序往往都會有大量的函數(shù)和過程。而且更為麻煩的是,其中的每一個(gè)都需要進(jìn)行稍許錯(cuò)誤檢查。
當(dāng)創(chuàng)建過程/函數(shù)時(shí),應(yīng)該總要考慮這樣的一個(gè)問題:“假如一些懷有惡意的人故意向函數(shù)或過程傳遞進(jìn)各種怪異的值,這段剛剛創(chuàng)建的代碼如何能自保并且讓計(jì)算機(jī)也能免受破壞呢?”然后,編寫代碼來檢查這些惡意數(shù)據(jù)以保護(hù)自身免受這些數(shù)據(jù)的破壞。
舉個(gè)例子。我們的這個(gè)太空游戲的主要目標(biāo)是殺掉外星人并積分,所以我們需要一個(gè)過程來更改分?jǐn)?shù)。而且,當(dāng)加分時(shí),我們需要調(diào)用一個(gè)例程來實(shí)現(xiàn)分?jǐn)?shù)上星光閃爍的效果。如下所示的是第一個(gè)過程:
Void change_score(short num_points)
{
score += num_points;
make_sparkles_on_score();
}
到目前為止還不錯(cuò)�,F(xiàn)在請思考一下:這里可能出現(xiàn)的錯(cuò)誤是什么呢?
首先,一個(gè)很明顯的問題是:如果 num_points 是負(fù)值該如何呢?我們能讓玩家的分?jǐn)?shù)降低么?就算我們能降低分?jǐn)?shù),但在我之前給出的關(guān)于該游戲的描述中,沒有提到過失分。而且,游戲應(yīng)該有趣,但失分無論如何不能算是一個(gè)有趣的事情。所以,我們將分?jǐn)?shù)負(fù)值視為一個(gè)錯(cuò)誤并必須要捕獲。
上述錯(cuò)誤相對容易,但這里有一個(gè)很微妙的問題(也是我在游戲中經(jīng)常要處理的)。如果 num_points 為零又會怎么樣呢?
這是一個(gè)很似是而非的情景。還記得么,我們會在每個(gè) wave 結(jié)束時(shí)根據(jù)玩家完成速度的快慢給一個(gè)獎勵分?jǐn)?shù)。如果玩家速度極慢,我們是否應(yīng)該給他一個(gè)值為零的獎勵分?jǐn)?shù)呢?在凌晨三點(diǎn),調(diào)用 change_score 并傳遞值 0,這完全可行。
現(xiàn)在的問題是我們可能不想讓計(jì)分板在顯示的數(shù)值沒有變化時(shí)仍舊五顏六色地閃個(gè)不停。所以我們要先捕獲這個(gè)問題。讓我們嘗試如下代碼:
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();
}
好了,情況好多了。
請注意這是很簡單的一個(gè)函數(shù)。里面并沒有用到任何極受新手推崇的新奇指針。如果要傳遞數(shù)組或指針,那么最好小心錯(cuò)誤和壞數(shù)據(jù)的出現(xiàn)。
這樣做的好處并不僅僅限于讓程序免遭破壞。好的錯(cuò)誤檢查還能讓調(diào)試更為迅速。假設(shè),您知道寫入的數(shù)據(jù)超出了數(shù)組的范圍,為了發(fā)現(xiàn)可能出現(xiàn)的錯(cuò)誤,您需要詳細(xì)檢查代碼。若所查看的這個(gè)過程中的錯(cuò)誤檢查均已就緒,那么就無需花很多時(shí)間去專門通查它來尋找錯(cuò)誤。
這種做法將節(jié)省大量時(shí)間,而且還能重復(fù)。還是那句話,時(shí)間是我們所擁有的寶貴資源。
提示 5:“不成熟的優(yōu)化是麻煩的根源” —— Donald Knuth
上述格言非我個(gè)人所造,它可以在 Wikipedia 中找到,所以必定是十分睿智的。
除非是想找別人麻煩,否則編寫代碼的首要目標(biāo)就是簡明性。簡單的代碼更易于編寫、易于日后理解,也更易于調(diào)試。
優(yōu)化與簡明性是相悖的。但有時(shí),卻必須要進(jìn)行優(yōu)化,在游戲中尤其如此。這個(gè)問題至關(guān)重要,您可能直到用解析器實(shí)際對工作代碼進(jìn)行測試時(shí)才會意識到需要進(jìn)行優(yōu)化。(解析器 是一種程序,用來監(jiān)視其他程序并找出該程序使用不同的調(diào)用所花費(fèi)的時(shí)間。這些都是很棒的程序。您可以找一個(gè)來試試。)
每次當(dāng)我優(yōu)化游戲時(shí),常常都禁不住會大出所料。我十分擔(dān)心的那些代碼總是問題不大,相反,我覺得萬無一失的代碼反倒會運(yùn)行得十分緩慢。由于我對運(yùn)行速度的快慢并沒有什么概念,在獲得實(shí)際數(shù)據(jù)之前我所進(jìn)行的優(yōu)化根本就是浪費(fèi)時(shí)間。比浪費(fèi)時(shí)間更糟糕的是它還讓代碼變得有些混亂。
這個(gè)規(guī)則看來很難遵守。但,如果規(guī)則很容易,它也就稱不上規(guī)則了。好的程序員大都更痛恨將原本可以運(yùn)行迅速的代碼弄得臃腫笨拙。
但好消息是,在我不斷 “該這樣不該那樣的” 布道式的介紹中, 這是惟一的一個(gè)您可以稍微懈怠一些的地方!
請讓自己編寫的代碼盡量整潔和有效一些吧。在后面的優(yōu)化階段,可能需要將其變得面目全非。所以如非必要,請慎重。
說到傷害,接下來,就來看看最后的這條建議。
提示 6:不要一知半解、自作聰明。
您可能聽說過 IOCCC 吧,即 International Obfuscated C Code Contest。大家都知道,C 和 C++,不管其優(yōu)勢如何卓越,都會最終導(dǎo)致編寫的代碼噩夢般地復(fù)雜。這個(gè)比賽就是要通過評選出最離譜的代碼來展示簡明代碼的價(jià)值,真是別具匠心。
讓我們來看看在您自認(rèn)為具有了編程的全部知識并甘愿冒險(xiǎn)的情況下,您能制造什么樣的麻煩。足夠的知識讓您信心百倍地將十行代碼壓縮進(jìn)一行代碼內(nèi)。付出的代價(jià)就是您絕對無法快速修復(fù)其中可能存在的 bug。
這里所需吸取的教訓(xùn)就是如果您所編寫的代碼要求您必須具有有關(guān)復(fù)雜優(yōu)先規(guī)則的詳細(xì)知識或讓您不得不翻看某些書的后面章節(jié)才能弄清來龍去脈,那么您在編寫這段代碼時(shí)就犯了一知半解、自作聰明的毛病了。
每個(gè)人對代碼的復(fù)雜性都有自己的容忍程度。就我個(gè)人而言,我編寫的程序往往呈比較典型的保守風(fēng)格。我個(gè)人認(rèn)為,如果一段 C 代碼需要您必須知道 i++ 和 ++i 之間的差別,那么這段代碼就過于復(fù)雜了。
您盡可以把我想象成一個(gè)循規(guī)蹈矩的人。沒錯(cuò),我的確如此。但循規(guī)蹈矩卻可以讓我花很少的時(shí)間就可以讀懂我的代碼。
結(jié)束語
至此,您可能會想:“哇哦,真是浪費(fèi)時(shí)間。您介紹的所有這些東西都是顯而易見,盡人皆知的。為何還多此一舉,寫這樣的文章呢?” 實(shí)際上,我很希望您會這么想,因?yàn)檫@意味著您已經(jīng)進(jìn)步了,變得明智了。這很好。
但不要錯(cuò)認(rèn)為所有這些內(nèi)容對每個(gè)人都是不言自明的。事實(shí)并非如此。糟糕的代碼隨處可見,但實(shí)際上這些代碼本不應(yīng)如此。
如果您正在努力編寫大量代碼并想讓自己不受其所累。那么就請讓代碼盡量簡單明了一些,這樣,您就可以節(jié)省大量時(shí)間和免受很多挫折。
|
|