整洁代码

来源:互联网 发布:净化网络环境的文章 编辑:程序博客网 时间:2024/05/21 09:45
分类:C/C++,JAVA,技术启蒙 | 作者:酷~行天下 | 发表于2011/09/14 5条评论 1,170 views

程序写出来是为了让人看懂它的算法,附带告诉计算机如何执行。

                                                                            ————Abelson & Sussman

好的代码优雅高效,整洁的代码如同散文,读起来酣畅淋漓;而读坏的代码犹如陷入沼泽,目光所及,无比绝望。整洁的代码,堪称人见人爱的艺术品;就像武术有不同的流派,整洁代码同样有不同的流派,这里要分享的是一些适用于面向对象的武林秘籍:

一、清晰而有意义的命名。

1) 名副其实, 比如:

帮助
+ expand source
1
2
intd; // 消逝的时间,以日计算
intelapsedTimeInDays;

第一种写法,看似简单,但是,不要忽视掉后面的注释,更多的时候,糟糕的命名需要更多的注释来说明,而能体现本意的名称能让人更容易理解和修改。

2)命名区分要有意义,比如:

帮助
+ expand source
1
2
3
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

这三个函数,除了作者本人,估计没人知道有什么区别。

3) 使用能读得出来的名称,对比下面两段代码:

帮助
+ expand source
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
//1)
classDtaRcrd102 {
    privateDate genymdhms;
    privateDate modymdhms;
    privatefinal String pszqint = "102";
    //…………
};
 
//2)
classCustomer {
    privateDate generationTimestamp;
    privateDate modificationTimestamp;;
    privatefinal String recordId = "102";
    //…………
};

如果不说genymdhms是代表生成日期,年、月、日、时、分、秒,你能猜到它什么意思吗,如果不知道这个函数名代表什么意思,那你又如何在后续代码中记住这个函数名,想想都痛苦,相反,第二种写法显而易见了:是用来生成时间戳的。

   这里还包含一层意思是:不要嫌麻烦就自造词,看看上边第一种代码的类名,妈的,又得猜了!

4) 使用能搜索的词,具体例子:

帮助
+ expand source
01
02
03
04
05
06
07
08
09
10
11
12
13
14
//1)
for(intj = 0; j < 34; j++) {
    s += (t[j] * 4) / 5;
}
 
//2)
intrealDaysPerIdealDay = 4;
constint WORK_DAYS_PER_WEEK = 5;
intsum = 0;
for(intj=0; j < NUMBER_OF_TASKS; j++) {
    intrealTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    intrealTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}

如果从代码中间突然冒出一段第一种的代码,你知道它什么意思吗,34, 4, 5 各代表什么意思,天知道,虽然第二种代码,type多了点,但是,回去看看文章的第一句话;不仅如此,维护代码时(维护代码当然要搜索),类似纯数字这样的代码难以维护,如果维护出现差错,就不是多打几个字母的损失了。

   一条可以参考经验是:

            “单字母名称用于短方法中的本地变量,名称长短应与其作用域大小相对应。”

   要是在三四行的for循环里,比如for (i = 0; i < n; i++) ,将i换成一个长名字,那你太欠扁了.

5) 一些约定俗称的用法

类名和对象应该是名词或名词短语,比如:Customer, WikiPage, Account, 和 AddressParser. 避免使用Manager, Processor, Data, or Info 这样的类名。类名不该是动词。

    方法名应当是动词或动词短语,比如:postPayment, deletePage.

6) 添加有意义的语境

将变量名:firstName, lastName, street, houseNumber, city, state, and zipcode等放到一起,你知道这是一组地址,但是,如果你在某个方法里看到一个孤零零的state变量呢?你还会认为这个是某个地址的一部分吗?

    对比下面两段代码,感觉一下,哪个更舒服:

帮助
+ expand source
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//1)
privatevoid printGuessStatistics(charcandidate, intcount) {
    String number;
    String verb;
    String pluralModifier;
    if(count == 0) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }elseif (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }else{
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
    String guessMessage = String.format(
        "There %s %s %s%s", verb, number, candidate, pluralModifier);
    print(guessMessage);
}
 
//2)
publicclass GuessStatisticsMessage {
    privateString number;
    privateString verb;
    privateString pluralModifier;
    publicString make(charcandidate, intcount) {
        createPluralDependentMessageParts(count);
        returnString.format(
            "There %s %s %s%s",
            verb, number, candidate, pluralModifier );
    }
 
    privatevoid createPluralDependentMessageParts(intcount) {
        if(count == 0) {
            thereAreNoLetters();
        }elseif (count == 1) {
            thereIsOneLetter();
        }else{
            thereAreManyLetters(count);
        }
    }
 
    privatevoid thereAreManyLetters(intcount) {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
 
    privatevoid thereIsOneLetter() {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }
    privatevoid thereAreNoLetters() {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

二、函数应该遵守的原则

1) 函数要短小,要更短小

好的函数应该20行封顶,一般多于十几行就该考虑分割函数了。譬如这个例子:

帮助
+ expand source
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1)
publicstatic String renderPageWithSetupsAndTeardowns(
    PageData pageData, booleanisSuite
    )throwsException {
    booleanisTestPage = pageData.hasAttribute("Test");
    if(isTestPage) {
        WikiPage testPage = pageData.getWikiPage();
        StringBuffer newPageContent = newStringBuffer();
        includeSetupPages(testPage, newPageContent, isSuite);
        newPageContent.append(pageData.getContent());
        includeTeardownPages(testPage, newPageContent, isSuite);
        pageData.setContent(newPageContent.toString());
    }
    returnpageData.getHtml();
}
 
//2)
publicstatic String renderPageWithSetupsAndTeardowns(
    PageData pageData, booleanisSuite) throwsException {
    if(isTestPage(pageData))
        includeSetupAndTeardownPages(pageData, isSuite);
    returnpageData.getHtml();
}

虽然第一种已经够短,为什么不更短些写呢,譬如缩短成第二种;

2)一个函数只做一件事,譬如下边这个例子:

帮助
+ expand source
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
importjava.util.*;
publicclass GeneratePrimes {
    publicstatic int[] generatePrimes(intmaxValue)
    {
        if(maxValue >= 2)// the only valid case
        {
            // declarations
            ints = maxValue + 1;// size of array
            boolean[]
            // initialize array to true.
            for(i = 0; i < s; i++)
                f[i] = true;
            // get rid of known non-primes
            f[0] = f[1] = false;
            // sieve
            intj;
            for(i = 2; i < Math.sqrt(s) + 1; i++)
            {
                if(f[i]) // if i is uncrossed, cross its multiples.
                {
                    for(j = 2* i; j < s; j += i)
                        f[j] = false;// multiple is not prime
                }
            }
            // how many primes are there?
            intcount = 0;
            for(i = 0; i < s; i++)
            {
                if(f[i])
                    count++;// bump count.
            }
            int[] primes = newint[count];
            // move the primes into the result
            for(i = 0, j = 0; i < s; i++)
            {
                if(f[i]) // if prime
                    primes[j++] = i;
            }
            returnprimes; // return the primes
        }
        else// maxValue < 2
            returnnew int[0];// return null array if bad input.
    }
}
 
//2)
publicclass PrimeGenerator
{
    privatestatic boolean[] crossedOut;
    privatestatic int[] result;
    publicstatic int[] generatePrimes(intmaxValue)
    {
        if(maxValue < 2)
            returnnew int[0];
        else
        {
            uncrossIntegersUpTo(maxValue);
            crossOutMultiples();
            putUncrossedIntegersIntoResult();
            returnresult;
        }
    }
 
    privatestatic void uncrossIntegersUpTo(intmaxValue)
    {
        crossedOut = newboolean[maxValue + 1];
        for(inti = 2; i < crossedOut.length; i++)
            crossedOut[i] = false;
    }
 
    privatestatic void crossOutMultiples()
    {
        intlimit = determineIterationLimit();
        for(inti = 2; i <= limit; i++)
            if(notCrossed(i))
                crossOutMultiplesOf(i);
    }
 
    privatestatic int determineIterationLimit()
    {
        doubleiterationLimit = Math.sqrt(crossedOut.length);
        return(int) iterationLimit;
    }
 
    privatestatic void crossOutMultiplesOf(inti)
    {
        for(intmultiple = 2*i;
            multiple < crossedOut.length;
            multiple += i)
            crossedOut[multiple] = true;
    }
 
    privatestatic boolean notCrossed(inti)
    {
        returncrossedOut[i] == false;
    }
 
    privatestatic void putUncrossedIntegersIntoResult()
    {
        result = newint[numberOfUncrossedIntegers()];
        for(intj = 0, i = 2; i < crossedOut.length; i++)
            if(notCrossed(i))
                result[j++] = i;
    }
 
    privatestatic int numberOfUncrossedIntegers()
    {
        intcount = 0;
        for(inti = 2; i < crossedOut.length; i++)
            if(notCrossed(i))
                count++;
        returncount;
    }
}

例子1可以改写为例子2,generatePrimes函数被切分为declarations, initializations和sieve等区段,这就是函数做事太多的明显征兆,只做一件事的函数无法被合理地切分为多个区段。当然,上边第一段还有其他糟糕的缺陷,譬如注释过多,命名太差等。

3)每个函数一个抽象层级

简单说,就是,让代码拥有自顶向下的阅读顺序,这样,每个函数后面都跟着位于下一个抽象层级的函数,这样在查看函数列表时,就能循抽象层级向下阅读了,这个叫做向下规则。

举个例子,大概就是:

程序要求:要实现A,就先实现B,然后再实现C,再实现D。

就可以,函数1:要实现A就实现B;      函数2:要实现B就实现C;       函数3:要实现C就实现D

4)函数参数,没有最佳,其次是一,再次是二,避免三参数

includeSetupPage()要比includeSetupPageInfo(newPage-Content)易于理解,参数与函数名处于不同的抽象层级,它要求你了解目前并不特别重要的细节,这个很烦人。从测试的角度,编写能确保各种参数的各种组合运行正常的测试用例非常困难,相反,如果没有参数,那就小菜一碟了。然而,并不是说让所有函数都没有参数,事实上这是不可能的,很多转换函数都必须要有参数,这里要说的是,尽量压缩函数参数,以防后患。

5)抽离Try/Catch代码块

Try/Catch非常实用,但是却搞乱了代码结构,把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数。看看下边这种写法,是不是好多了。

帮助
+ expand source
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
publicvoid delete(Page page) {
    try{
        deletePageAndAllReferences(page);
    }
    catch(Exception e) {
        logError(e);
    }
}
 
privatevoid deletePageAndAllReferences(Page page) throwsException {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
 
privatevoid logError(Exception e) {
    logger.log(e.getMessage());
}

来看看一个非常漂亮的,遵循上边所有原则的代码:(实在太漂亮了)

帮助
+ expand source
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
packagefitnesse.html;
 
importfitnesse.responders.run.SuiteResponder;
importfitnesse.wiki.*;
 
publicclass SetupTeardownIncluder {
    privatePageData pageData;
    privateboolean isSuite;
    privateWikiPage testPage;
    privateStringBuffer newPageContent;
    privatePageCrawler pageCrawler;
 
    publicstatic String render(PageData pageData) throwsException {
        returnrender(pageData, false);
    }
 
    publicstatic String render(PageData pageData, booleanisSuite)
        throwsException {
            returnnew SetupTeardownIncluder(pageData).render(isSuite);
    }
 
    privateSetupTeardownIncluder(PageData pageData) {
        this.pageData = pageData;
        testPage = pageData.getWikiPage();
        pageCrawler = testPage.getPageCrawler();
        newPageContent = newStringBuffer();
    }
 
    privateString render(booleanisSuite) throwsException {
        this.isSuite = isSuite;
        if(isTestPage())
            includeSetupAndTeardownPages();
        returnpageData.getHtml();
    }
 
    privateboolean isTestPage() throwsException {
        returnpageData.hasAttribute("Test");
    }
 
    privatevoid includeSetupAndTeardownPages() throwsException {
        includeSetupPages();
        includePageContent();
        includeTeardownPages();
        updatePageContent();
    }
 
    privatevoid includeSetupPages() throwsException {
        if(isSuite)
            includeSuiteSetupPage();
        includeSetupPage();
    }
 
    privatevoid includeSuiteSetupPage() throwsException {
        include(SuiteResponder.SUITE_SETUP_NAME,"-setup");
    }
 
    privatevoid includeSetupPage() throwsException {
        include("SetUp","-setup");
    }
 
    privatevoid includePageContent() throwsException {
        newPageContent.append(pageData.getContent());
    }
 
    privatevoid includeTeardownPages() throwsException {
        includeTeardownPage();
        if(isSuite)
            includeSuiteTeardownPage();
    }
 
    privatevoid includeTeardownPage() throwsException {
        include("TearDown","-teardown");
    }
 
    privatevoid includeSuiteTeardownPage() throwsException {
        include(SuiteResponder.SUITE_TEARDOWN_NAME,"-teardown");
    }
 
    privatevoid updatePageContent() throwsException {
        pageData.setContent(newPageContent.toString());
    }
 
    privatevoid include(String pageName, String arg) throwsException {
        WikiPage inheritedPage = findInheritedPage(pageName);
        if(inheritedPage != null) {
            String pagePathName = getPathNameForPage(inheritedPage);
            buildIncludeDirective(pagePathName, arg);
        }
    }
 
    privateWikiPage findInheritedPage(String pageName) throwsException {
        returnPageCrawlerImpl.getInheritedPage(pageName, testPage);
    }
 
    privateString getPathNameForPage(WikiPage page) throwsException {
        WikiPagePath pagePath = pageCrawler.getFullPath(page);
        returnPathParser.render(pagePath);
    }
 
    privatevoid buildIncludeDirective(String pagePathName, String arg) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .append(" .")
            .append(pagePathName)
            .append("\n");
    }
}

三、代码格式

1)垂直格式

首先是垂直尺寸,源代码文件该有多大,多数Java源文件有多大,看看下边这张图:

图中涉及7个不同项目,贯穿方块的直线两端显示这些项目中最小和最大的文件长度,方块表示在平均值以上或以下三分之一文件的长度。方块中间就是平均数。可以看到FitNesse项目文件平均尺寸是65行,最大是400行,最小是6行。Junit,FitNesses,Time and Money由相对较小文件组成,没有一个超过500行,多数小于200行。Tormat和Ant则有些达到数千行,将近一半文件长于200行。

这意味着:我们可以用大多数为200行、最长为500行的单个文件构造出出色的系统。尽管这不是什么不可以违背的原则,但是也应该乐于接受,短文件总是比长文件易于理解。

2)横向格式

一行代码该有多宽,大多数人都有自己的标准,看看典型的程序中代码行的宽度,上图:

明显,大多数programmer更喜欢用短代码,图中可以看到,20~80个字符长度的代码分布非常平稳,更长到100或120也可以,这不是什么规则,也许有些人显示器一行可以显示200个字符,但是,培养一个好习惯,不要超过120个字符,想想有谁愿意看代码的时候左右拉滚动条呢。

3)团队规则

每个程序员都有自己喜欢的格式规则,但如果在一个团队工作,就是团队说了算。一组开发者应当认同一种格式风格,每个成员都应该采用那种风格。软件应该拥有一以贯之的风格,而不是,一看就是一大票意见相左的个人所写。好的软件系统是由一系列读起来不错的代码文件组成。它们需要拥有一致和顺畅的风格。绝对不要用各种不同的风格来编写源代码,这样会增加复杂度。

原创粉丝点击