編碼之道【翻譯】

編碼之道
2005-12-30 李欣蔚 翻譯

引入

這篇文章記述了我多年開發所使用的編碼風格.我的風格並不具有廣泛性.事實上,我不知道還有誰和我編碼的怪異風格相近.但是我喜歡它並且想和你們分享(你們真是幸運的傢伙!).我在所有語言都使用它,包括:C,C++,JAVA,C#,Python,…

如果你想馬上快速地瀏覽一下此種風格,翻到本頁底部,看看源代碼到底是如何用我的deWitters風格書寫的,你就能馬上明白它們是如何好了.


爲什麼需要編碼風格?

每一位程序員都使用某種編碼風格--好的或者糟糕的.一種編碼風格能夠給予代碼一致的外觀.它能夠讓算法更加清晰或者更加複雜.下面是使用可靠的編碼風格的2個主要原因:

  1. 開發清晰和可讀的代碼,這能夠讓閱讀它的其它人能夠迅速理解它的意思.更加重要的是當你回頭看一些一年前寫的代碼的時候,你不會開始想:”我記不清楚了,這簡直是在浪費時間…”
  2. 當團隊協作時,最好是所有人都使用統一的編碼風格,這樣代碼都具有統一的外觀.


因爲大多數代碼都是我自己開發的,我不需要考慮第二個原因.另外還因爲我非常固執,我不會採用其它人的風格.這是爲什麼我的風格是完全能充分產生清晰可讀代碼的原因.


基本規則


我在下面的規則中列舉了編碼風格的重要的幾個方面:


  1. 所有代碼都應該儘可能的可理解
  2. 所有代碼應該儘可能的可讀,除非它和上面的規則相沖突
  3. 所有代碼應該儘可能簡單,除非它和上面的規則相沖突

對待上面的規則的最好方式是讓所有事情儘可能的簡單,除非具有足夠的可理解性和可讀性.引用愛因斯坦的話:
讓所有的事情儘可能的簡單,但不要過分簡單.


由於現代編程語言的誕生,編寫可理解和閱讀的代碼變得有可能.完全使用彙編語言的時代已經離我們遠去.因此我的編碼風格試圖儘可能的接近自然語言.你會說讀我的代碼就像讀書一樣.這也可能是爲什麼我很少爲我的代碼寫文檔的原因.我幾乎從不寫文檔!我甚至認爲寫文檔是”有害的”的(並且寫的也不酷)只有當我寫些古怪東西的時候我才用註釋解釋爲什麼.在下認爲,註釋應該從不解釋你的代碼做什麼;而應該讓你的代碼說它自己是做什麼的.


任何傻瓜都能寫計算機能夠理解的代碼.優秀的程序員才能寫人能夠理解的代碼.
馬丁 弗諾爾


標識符


讓我們以編碼風格中最重要的主題--標識符—作爲開始.所有標識符和你的其它代碼以及註釋都應該用英語來書寫.軟件項目從一個人傳給另一個人,從一家公司傳給世界另一端的另一家公司是非常尋常的.正因爲你並不知道你的代碼會傳給誰,所以將它們全部用英語書寫.


變量


變量命名應該全部用小寫字母並用下劃線分隔單詞.它更符合書寫習慣並因爲它最具有可讀性.下劃線恰好代替了在書寫習慣中的空格.相信我,一個叫做”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;

命名控件,包

命名空間或者包應該用小寫,而且不用任何下劃線.爲你寫的每一個模塊或者層使用命名空間,這樣在代碼中不同的層更加清晰.

設計

當我開始項目的時候我並不做太多前端設計.我只要在我的腦海中有一個全局結構就開始編寫代碼.代碼進化—無論你喜歡還是不喜歡—給代碼進化的機會.

進化的代碼因爲着重寫糟糕的代碼,並且在某些編碼後你的代碼將變糟糕.我使用下面的原則來保持代碼中的良好結構.


  1. 當函數太長,將它劃分爲一些更小的輔助函數.
  2. 如果一個類包含太多的成員和方法,將這個類劃分爲輔助類並在主類中包含輔助類(不要在這裏使用繼承!)確保輔助類不會引用或者由於任何原因引用主類
  3. 當模塊包含太多的類,將它劃分爲更多的模塊,並且高層模塊使用底層模塊.
  4. 當你實現了功能或者修改了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
}

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章