編碼之道
2005-12-30 李欣蔚 翻譯
引入
這篇文章記述了我多年開發所使用的編碼風格.我的風格並不具有廣泛性.事實上,我不知道還有誰和我編碼的怪異風格相近.但是我喜歡它並且想和你們分享(你們真是幸運的傢伙!).我在所有語言都使用它,包括:C,C++,JAVA,C#,Python,…
如果你想馬上快速地瀏覽一下此種風格,翻到本頁底部,看看源代碼到底是如何用我的deWitters風格書寫的,你就能馬上明白它們是如何好了.
爲什麼需要編碼風格?
每一位程序員都使用某種編碼風格--好的或者糟糕的.一種編碼風格能夠給予代碼一致的外觀.它能夠讓算法更加清晰或者更加複雜.下面是使用可靠的編碼風格的2個主要原因:
- 開發清晰和可讀的代碼,這能夠讓閱讀它的其它人能夠迅速理解它的意思.更加重要的是當你回頭看一些一年前寫的代碼的時候,你不會開始想:”我記不清楚了,這簡直是在浪費時間…”
- 當團隊協作時,最好是所有人都使用統一的編碼風格,這樣代碼都具有統一的外觀.
因爲大多數代碼都是我自己開發的,我不需要考慮第二個原因.另外還因爲我非常固執,我不會採用其它人的風格.這是爲什麼我的風格是完全能充分產生清晰可讀代碼的原因.
基本規則
我在下面的規則中列舉了編碼風格的重要的幾個方面:
- 所有代碼都應該儘可能的可理解
- 所有代碼應該儘可能的可讀,除非它和上面的規則相沖突
- 所有代碼應該儘可能簡單,除非它和上面的規則相沖突
對待上面的規則的最好方式是讓所有事情儘可能的簡單,除非具有足夠的可理解性和可讀性.引用愛因斯坦的話:
讓所有的事情儘可能的簡單,但不要過分簡單.
由於現代編程語言的誕生,編寫可理解和閱讀的代碼變得有可能.完全使用彙編語言的時代已經離我們遠去.因此我的編碼風格試圖儘可能的接近自然語言.你會說讀我的代碼就像讀書一樣.這也可能是爲什麼我很少爲我的代碼寫文檔的原因.我幾乎從不寫文檔!我甚至認爲寫文檔是”有害的”的(並且寫的也不酷)只有當我寫些古怪東西的時候我才用註釋解釋爲什麼.在下認爲,註釋應該從不解釋你的代碼做什麼;而應該讓你的代碼說它自己是做什麼的.
任何傻瓜都能寫計算機能夠理解的代碼.優秀的程序員才能寫人能夠理解的代碼.
馬丁 弗諾爾
標識符
讓我們以編碼風格中最重要的主題--標識符—作爲開始.所有標識符和你的其它代碼以及註釋都應該用英語來書寫.軟件項目從一個人傳給另一個人,從一家公司傳給世界另一端的另一家公司是非常尋常的.正因爲你並不知道你的代碼會傳給誰,所以將它們全部用英語書寫.
變量
變量命名應該全部用小寫字母並用下劃線分隔單詞.它更符合書寫習慣並因爲它最具有可讀性.下劃線恰好代替了在書寫習慣中的空格.相信我,一個叫做”RedPushButton”的並不能像”red_push_button”一樣具有容易並快速的閱讀.
如果你想讓變量具有可理解性,你必須給它們取明顯的名字.非常清楚的是變量都是表示某些”對象”或者”值”的,因此爲它們命名.但不要在諸如kuidsPrefixing vndskaVariables ncqWith ksldjfTheir nmdsadType上面浪費時間,因爲它們不可讀也不清晰.如果你有一個變量”age”,它很明顯是int或者unsigned short.如果它是”filename”,很明顯它必須是字符串.簡單!某些時候對於某些變量,如果你在它的命名中包含類型將具有更好的可讀性.比如對於GUI按鈕:”play_button”或者”cancel_button”.
下面是某些能夠增加變量可讀性的前綴和後綴.下面列出了非常常用的一些:
is_,has_
對於所有boolean值使用這些前綴,這樣就不會在類型上出錯.它同樣對
於if語句非常適合.
the_
對於所有的全局變量使用”the_”,它能夠非常清楚的表示這裏只有一
個.
_count
所以_count表示元素的個數.不要使用複數形式”bullets”代替”bullet_count”,因爲複數將表示數組.
數組或者表示列表的其它變量必須寫成複數形式,比如enemies, walls 和 weapons.儘管如此,你不需要對所有數組類型使用複數,因爲某些數組並不真正表示項目序列.比如”char filename[64]”後者”byte buffer[128]”.
Const或者Final
Const或者final必須用大寫形式表示,並使它們更加具有可讀性.比如MAX_CHILDREN,X_PADDING或者PI.這種用法很廣泛並且應該被用來避免和普通變量相混淆.
你能夠在常量名字中使用MAX和MIN表示極限值.
類型
類型定義了變量的分類.它是有點抽象,所以我不能夠使用英語作爲如何書寫它們的參考.但是我們應該明確區分它們和其它標識符之間的差別.所以對於類型,我使用UpperCamelCase.對於每一個class,struct,enum或者其它在你的變量聲明之前的事物都使用UpperCamelCase.
以此種方式命名你的類型能夠讓你對普通變量使用同樣的名字,比如
HelpWindow help_window;
FileHeader file_header;
Weapon weapons[ MAX_WEAPONS ];
程序流程
if, else if, else
書寫if語句有多種方式.讓我們從圓括號開始.這裏有3種主要的放置你的圓括號的方式:
if(condition)
if (condition)
if( condition )
我從來沒有在英語正文中看到圓括號像例1一樣放置的,所以爲什麼我要像那樣編碼呢?哪些單詞恰好被不適當的分隔了.第二個例子將條件和圓括號放在一起代替了if語句,同時圓括號居然是if語句的一部分而不是條件語句的一部分.只有最後一個例子有優點,它具有更好的圓括號結構的一個概貌.
if (!((age > 12) && (age < 18)))
if( !((age > 12) && (age < 18)) )
就個人而言,我本應該以不同方式寫這段代碼,但它只是作爲一個示例.
現在對於花括號應該怎麼辦呢?不要使用它們!不幸的是C,C++,JAVA或者C#不允許這樣做,只有Python允許.所以我們不能丟掉它們,但我們能做什麼能夠讓它看起來像Python程序一樣簡潔呢?
if( condition ) {
statements;
}
else if( condition ) {
statements;
}
else {
statements;
}
當條件過長,你必須將它們斷行.試着在操作符之前將它們斷開,並且條件保持最低的關聯.在下一行與前面對齊並使用縮排來展示嵌套結構.不要把花括號正好寫在條件的後面,在這種情況下將它們緊挨下一行使子塊清晰:
if( (current_mayor_version < MIN_MAYOR_VERSION)
|| (current_mayor_version == MIN_MAYOR_VERSION
&& current_minor_version < MIN_MINOR_VERSION) )
{
update();
}
當if條件只有一條語句的時候,你可以不使用花括號,但是要確保你將語句寫在下一行,除了它是一條return語句或者break語句.
if( bullet_count == 0 )
reload();
if( a < 0 ) return;
while
While循環與if結構書寫一樣.我爲每一個縮進使用4個空格
while( condition ) {
statements;
}
對於do-while循環,將while與緊鄰的花括號放在相同一行.如果在子塊的開始或者結尾的while就不會有混淆.
do {
statements;
} while( condition )
for
for循環一生有且僅有的意圖就是迭代.這就是它要做的!for循環常常能夠被while循環代替,但是請不要這樣做.當你對某些元素進行迭代的使用,試着使用’for’,只有當它不能解決問題的時候,才使用’while’.’for’結構非常直觀:
for( int i = 0; i < MAX_ELEMENTS; i++ ) {
statements;
}
使用I,j,k,l,m作爲迭代數字,’it’作爲對對象的迭代.
switch
Switch語句有與if和while結構相似的結構.唯一需要考慮的就是額外的標識符.同樣在break後面留出多餘的空格.
switch( variable ) {
case 0:
statements;
break;
case 1:
statements;
break;
default:
break;
}
Functions
函數
函數是用來幹事兒的,它們的名字應該清晰.因此,通常都包含一個動詞,沒有例外!使用與變量相同的命名方式,這意味所有小寫單詞由下劃線分隔開.這允許你在你的代碼中使用小段句子以便讓每個人都理解.
同樣確保函數做的事情與它的名字相符,不要過多,也不要太少.所以如果由一個函數叫做”load_resources”,確信它只是裝載資源而不會去初始化填充.某些時候,你圖方便就把初始化的事情放在load_resources中,因爲你在更高層已經調用它,但是這將在以後引起問題.我的deWiTTERS 風格使用非常少的註釋,所以一個函數應該明確的展示它的名字叫它做的事情.並且當一個函數返回某些東西,確信它的名字清晰的反映它將返回什麼.
某些函數以”陰和陽”對的形式出現,你應該統一你的命名方式.比如: get/set, add/remove, insert/delete, create/destroy, start/stop, increment/decrement, new/old, begin/end, first/last, up/down, next/prev, open/close, load/save, show/hide, enable/disable, resume/suspend等等.
下面是一個簡單的函數調用.在開始的圓括號的後面以及末尾的圓括號前面使用空格,就像if結構一樣.同樣在都好後面空格,就像使用英語一樣.
do_something( with, these, parameters );
當函數調用太長的時候,你應該將它們斷開爲幾行.將下一行與前面對齊,這樣結構非常明顯,並以都好斷開.
HWND hwnd = CreateWindow( "MyWin32App", "Cool application",
WS_OVERLAPPEDWINDOW,
my_x_pos, my_y_pos,
my_width, my_height
NULL, NULL, hInstance, NULL );
定義
下面是函數定義的例子:
bool do_something_today( with, these, parameters ) {
get_up();
go_to( work, car );
work();
go_to( home, car );
sleep();
return true;
}
確保你的代碼不會太長,或者按照linus的說法是:函數的最長長度與函數的複雜性以及縮進層次成反比”.所以,如果你有一個概念性的簡單函數,它是一個長(但是簡單)case語句,你就必須對許多不同的case做許多不同的小事情.儘管如此,如果你有一個複雜的函數,並且你懷疑一個天賦不佳的一年級高中生都不知道函數是什麼,你隨時應該堅持最大限制.使用描述命名的輔助函數(如果考慮到注重性能的情況,你能夠讓編譯器去inline它們,並且它會比你做的更好.)
類
對於class的命名方式我使用和類型一樣的UpperCamelCase.不要爲每個class都加上’C’前綴,那只是在浪費字節和時間而已.
對於任何事情,給class清晰並且明顯的名字.如果一個class是”Window”類的子類,將它命名爲”MainWindow”.
當創建一個新的class,記住任何事情都來自數據結構.
數據支配.如果你已經選擇了正確的數據結構並將事情組織得當,算法將通常是不證自明的.數據結構而不是算法是編程的中心.
Fred Brooks
繼承
“is a”關係應該使用繼承.”has a”應該使用包含.確保不要過分使用繼承.它本身是偉大的技術,但只有在被適當應用的情況.
成員
你應該明確成員和普通變量之間的差異.如果你不這樣做.你將在以後後悔.某些情況下將它們命名爲m_Member或者fMember.我喜歡使用對靜態成員使用my_member,對靜態普通變量使用our_member.這將在下面語句中非常不錯:
if( my_help_button.is_pressed() ) {
our_screen_manager.go_to( HELP_SCREEN );
}
應用於變量命名的其它規則同樣能夠可用於成員.這裏有一個問題我不能解決,那就是boolean成員.記住在boolean值中必須有”is”和”has”.當於”my_”結合的時候,你得到瘋狂的名字,比如”my_is_old”和”my_has_children”.我也沒有找到此問題的完美解決方案,所以如果你有任何建議,請發EMAIL給我.
你不應該將class的成員聲明爲public.某些時候它看起來能更快速的實現,並因此更好,但是你錯了(我也曾經歷過).你應該使用public的方法來取得class的成員.
方法
應用到函數的任何事物都能夠應用在方法上,記住名字中要包含動詞.確保在你的方法名字中不要包含class的名字.
代碼結構
將相似的行對齊,能夠讓你的代碼看起來更加統一:
int object_verts[6][3] = {
{-100, 0, 100}, {100, 0, 100}, { 100, 0, -100},
{-100, 11, 100}, (100, 0, -100}, {-100, 0, -100}
};
RECT rect;
rect.left = x;
rect.top = y;
rect.right = rect.left + width;
rect.bottom = rect.right + height;
千萬不要將多行寫在一行上面,除非你對此有好的理由.其它原因是相似的行應該爲聲明放每行一句:
if( x & 0xff00 ) { exp -= 16; x >>= 16; }
if( x & 0x00f0 ) { exp -= 4; x >>= 4; }
if( x & 0x000c ) { exp -= 2; x >>= 2; }
同種類型的相關變量能夠以相同語句聲明.這樣使代碼更加緊湊,並更加統一.但是不要將不相干的變量放在同一行.
int x, y;
int length;
命名控件,包
命名空間或者包應該用小寫,而且不用任何下劃線.爲你寫的每一個模塊或者層使用命名空間,這樣在代碼中不同的層更加清晰.
設計
當我開始項目的時候我並不做太多前端設計.我只要在我的腦海中有一個全局結構就開始編寫代碼.代碼進化—無論你喜歡還是不喜歡—給代碼進化的機會.
進化的代碼因爲着重寫糟糕的代碼,並且在某些編碼後你的代碼將變糟糕.我使用下面的原則來保持代碼中的良好結構.
- 當函數太長,將它劃分爲一些更小的輔助函數.
- 如果一個類包含太多的成員和方法,將這個類劃分爲輔助類並在主類中包含輔助類(不要在這裏使用繼承!)確保輔助類不會引用或者由於任何原因引用主類
- 當模塊包含太多的類,將它劃分爲更多的模塊,並且高層模塊使用底層模塊.
- 當你實現了功能或者修改了bug,通讀一遍你改變的整個文件,並確保所有事物都處於非常完美的狀態.
某些項目可能變大,非常大.處理這種增長的複雜性的方法是將你的項目分爲不通的層.實踐中,層作爲命名空間實現的.底層被高層所使用.所以每一層爲上一層提供功能.最高層爲用戶提供功能.
Files
文件
文件應該按照它包含的類的名字來命名.不要在一個文件中放多個class,這樣在你搜索某個類的時候你才知道在那裏去找.目錄結構應該表示命名空間.
.h文件結構
C或者C++頭文件顯示了實現的接口.這是在設計a.h文件時候的關鍵知識.在一個class中,首先定義能夠被其它類使用的”public”接口,然後定義所有”protected”方法和成員.人們使用類的非常重要的信息是使用首先顯示出來的方法.我不使用private方法和成員,這樣所有成員組織在class聲明的底部.這樣你能夠能夠快事看到類底部的內容.將方法以它們的意思進行分組.
/*
* license header
*/
#ifndef NAMESPACE_FILENAME_H
#define NAMESPACE_FILENAME_H
#include <std>
#include "others.h"
namespace dewitters {
class SomeClass : public Parent {
public:
Constructor();
~Destructor();
void public_methods();
protected:
void protected_methods();
int my_fist_member;
double my_second_member;
const static int MAX_WIDTH;
};
extern SomeClass the_some_class;
}
#endif
.java .cs文件結構
.java 或者 .cs文件並不提供class的界面,它們只包含實現.因爲數據結構比算法更加重要,所以在方法之前定義成員.當瀏覽代碼的時候,你能夠得到關於此class的數據成員的印象.相似的代碼應該組織在一起.
Here follows a sketchy overview of a .java or .cs file:
下面顯示了對.java或者.cs文件的一個粗略概覽:
/*
* license header
*/
package com.dewitters.example;
import standard.modules.*;
import custom.modules.*;
class SomeClass extends Parent {
public final int MAX_WIDTH = 100;
protected int my_first_member;
protected double my_second_member;
Constructor() {
}
Methods() {
}
}
笑話
某些人喜歡在他們的代碼中放些小笑話,而其它人憎恨這類搞笑分子.以我來看只要不影響代碼的可讀性和程序的執行,你可以隨便使用笑話.
deWiTTERS Style vs. others
這裏我將給你看些活生生的代碼.我偷了些別人的代碼,並以我自己的方式重寫.你可以自己判斷一下我的風格到底是好是壞.在我看來,你能夠快速的閱讀我的代碼,因爲它們更斷,並且所有標識符都被謹慎的命名.
如果你認爲你已看到過能擊敗我的風格的代碼,請給我寫EMAIL,並且我將會把它寫進更強的’deWiTTER’風格中,併發布在這裏.
Indian Hill C Style
/**//*
* skyblue()
*
* Determine if the sky is blue.
*/
int /**//* TRUE or FALSE */
skyblue()
...{
extern int hour;
if (hour < MORNING || hour > EVENING)
return(FALSE); /**//* black */
else
return(TRUE); /**//* blue */
}
/**//*
* tail(nodep)
*
* Find the last element in the linked list
* pointed to by nodep and return a pointer to it.
*/
NODE * /**//* pointer to tail of list */
tail(nodep)
NODE *nodep; /**//* pointer to head of list */
...{
register NODE *np; /**//* current pointer advances to NULL */
register NODE *lp; /**//* last pointer follows np */
np = lp = nodep;
while ((np = np->next) != NULL)
lp = np;
return(lp);
}
Rewritten to deWiTTERS Style:
bool sky_is_blue() ...{
return the_current_hour >= MORNING && the_current_hour <= EVENING;
}
Node* get_tail( Node* head ) ...{
Node* tail;
tail = NULL;
Node* it;
for( it = head; it != NULL; it = it->next ) ...{
tail = it;
}
return tail;
}
"Commenting Code" from Ryan Campbell
/**//*
* Summary: Determine order of attacks, and process each battle
* Parameters: Creature object representing attacker
* | Creature object representing defender
* Return: Boolean indicating successful fight
* Author: Ryan Campbell
*/
function beginBattle(attacker, defender) ...{
var isAlive; // Boolean inidicating life or death after attack
var teamCount; // Loop counter
// Check for pre-emptive strike
if(defender.agility > attacker.agility) ...{
isAlive = defender.attack(attacker);
}
// Continue original attack if still alive
if(isAlive) ...{
isAlive = attacker.attack(defender);
}
// See if any of the defenders teammates wish to counter attack
for(teamCount = 0; teamCount < defender.team.length; i++) ...{
var teammate = defender.team[teamCount];
if(teammate.counterAttack = 1) ...{
isAlive = teammate.attack(attacker);
}
}
// TODO: Process the logic that handles attacker or defender deaths
return true;
} // End beginBattle
Rewritten to deWiTTERS Style:
function handle_battle( attacker, defender ) ...{
if( defender.agility > attacker.agility ) ...{
defender.attack( attacker );
}
if( attacker.is_alive() ) ...{
attacker.attack( defender );
}
var i;
for( i = 0; i < defender.get_team().length; i++ ) ...{
var teammate = defender.get_team()[ i ];
if( teammate.has_counterattack() ) ...{
teammate.attack( attacker );
}
}
// TODO: Process the logic that handles attacker or defender deaths
}