编码之道【翻译】

编码之道
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
}

 
阅读更多
个人分类: 计算机-技术随感
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭