代碼整潔之道-理論

[TOC]

代碼整潔之道-理論

前言

學(xué)習(xí)中、工作中遇到很多亂七八糟的糟糕代碼,自己入門時(shí)也寫過不少糟糕代碼。在一個(gè)夜深人靜的晚上,思考人生,覺得要成為一名更好的程序員,那寫代碼的基本功就要扎實(shí)。

于是結(jié)合自己曾今看過的關(guān)于代碼設(shè)計(jì)的書,進(jìn)行了總結(jié),寫下這篇博客,作為日后編碼的參考文檔。本文以《代碼整潔之道》、《重構(gòu):改善既有代碼的設(shè)計(jì)》、《設(shè)計(jì)模式之禪》、《Head First設(shè)計(jì)模式》和《阿里巴巴Java開發(fā)手冊(cè)》為原始資料,總結(jié)了其中的核心內(nèi)容,并且在部分內(nèi)容中加入了自己的見解。

開篇之前,來幾句雞湯文補(bǔ)補(bǔ)身子。

編程不僅僅是寫代碼,而是一門語言設(shè)計(jì)的藝術(shù)。

大神級(jí)程序員是把系統(tǒng)當(dāng)做故事來講,動(dòng)聽優(yōu)雅,而不是當(dāng)成程序來寫。

溫馨提醒:以下關(guān)于代碼整潔的理論和建議對(duì)英語水平有一定要求的,尤其是命名和注釋的章節(jié)。所以這些理論和建議不能全部套用,要根據(jù)團(tuán)隊(duì)實(shí)際情況來斟酌運(yùn)用,適合自身團(tuán)隊(duì)的才是最好的。

一、優(yōu)雅代碼的層次

優(yōu)雅代碼具有這幾個(gè)特點(diǎn):可讀性、可復(fù)用、健壯性(魯棒性)、高擴(kuò)展、高性能。

1、第一層次:命名要好

優(yōu)雅代碼最基本的是要有良好的命名。良好的命名才有可讀性。

這個(gè)可以參考《代碼整潔之道》的第2章:有意義的命名《阿里巴巴Java開發(fā)手冊(cè)》。

這個(gè)是最基本的能力,團(tuán)隊(duì)中的程序員必須要學(xué)會(huì)的基礎(chǔ)能力。

2、第二層次:代碼結(jié)構(gòu)要清晰

清晰的代碼結(jié)構(gòu)才有可讀性、可復(fù)用。同時(shí)代碼要健壯(魯棒性),如需要判空、校驗(yàn)非法值等。

這個(gè)可以參考《代碼整潔之道》的第3、4、5、7、8章:函數(shù)、注釋、格式、錯(cuò)誤處理、邊界《阿里巴巴Java開發(fā)手冊(cè)》《重構(gòu):改善既有代碼的設(shè)計(jì)》。

這個(gè)也是最基本的能力,團(tuán)隊(duì)中的程序員必須要學(xué)會(huì)的基礎(chǔ)能力。

3、第三層次:熟悉6大設(shè)計(jì)原則

前面兩個(gè)層次只是關(guān)注如何編寫好的代碼行和代碼塊,如函數(shù)的恰當(dāng)構(gòu)造,函數(shù)之間如何互相關(guān)聯(lián)等。

第三層次將關(guān)注代碼組織的更高層面,即類。符合6大設(shè)計(jì)原則的類,可讀性、可復(fù)用、魯棒性、擴(kuò)展性和性能會(huì)更好。

這個(gè)可以參考《設(shè)計(jì)模式之禪》的第1、2、3、4、5、6章《代碼整潔之道》的第10章

這個(gè)是較高的能力要求,個(gè)人覺得,在企業(yè)級(jí)開發(fā)中,1-3年的程序員應(yīng)該要達(dá)到這一層次的能力。

4、第四層次:熟悉23種設(shè)計(jì)模式

第四層要求程序員能站在整個(gè)系統(tǒng)的角度去合理運(yùn)用這23種設(shè)計(jì)模式,對(duì)代碼進(jìn)行分包、分類、接口和類設(shè)計(jì)等架構(gòu)工作,使得代碼高擴(kuò)展。

這個(gè)可以參考《設(shè)計(jì)模式之禪》的第7-38章《Head First設(shè)計(jì)模式》。

這個(gè)是更高的能力要求,個(gè)人覺得,在企業(yè)級(jí)開發(fā)中,3-5年的高級(jí)程序員應(yīng)該要達(dá)到這一層次的能力。

5、第五層次:并發(fā)編程

代碼要高性能。

這個(gè)可參考并發(fā)編程的相關(guān)書籍,如《代碼整潔之道》中的附錄A:并發(fā)編程(p297)、《深入理解Java虛擬機(jī)》的第五部分:高效并發(fā)(p359)《Java并發(fā)編程的藝術(shù)》、《Java并發(fā)編程:核心方法與框架》、《Java程序性能優(yōu)化》。

這個(gè)能力要求就更高了。本人能力暫時(shí)不足,不好定義。先暫時(shí)定義成最高層次吧。

二、什么是糟糕的代碼

本人將代碼分為兩類:業(yè)務(wù)代碼、框架代碼。對(duì)于業(yè)務(wù)代碼,以上的特性(可讀性、可復(fù)用、健壯性(魯棒性)、高擴(kuò)展、高性能)都要兼顧,但有時(shí)候可以犧牲一部分性能來提高代碼的可讀性、健壯性(魯棒性)和高擴(kuò)展;對(duì)于框架代碼,可以犧牲一部分可讀性來實(shí)現(xiàn)更高的性能。

框架代碼一般都是大牛寫的,基本上不存在糟糕代碼。但是業(yè)務(wù)代碼則存在很多糟糕的代碼,所以本文中講的糟糕代碼指業(yè)務(wù)代碼。

不符合上面5個(gè)特性的代碼基本上都是糟糕的代碼。具體表現(xiàn)如下:

(一)命名糟糕

1、采用描述性名稱。

2、名稱沒有與抽象層級(jí)相符。

3、沒有使用標(biāo)準(zhǔn)命名法。如駝峰。

4、使用歧義的名稱。

5、沒有為較大作用范圍選用較長(zhǎng)名稱。

6、使用編碼。

7、名稱沒有說明副作用。即多做事情了,但名稱看不出來。

(二)函數(shù)(方法糟糕)

長(zhǎng)函數(shù)、有大量字符串、怪異不常見的數(shù)據(jù)類型和API、有太多不同層級(jí)的抽象、奇怪的字符串和函數(shù)調(diào)用、多重嵌套、用標(biāo)志來控制的if語句......《代碼整潔之道》p30頁有個(gè)例子可以感受一下糟糕代碼。其實(shí),在工作中維護(hù)舊系統(tǒng)代碼時(shí)、隔一段時(shí)間再看自己以前寫的代碼時(shí),也會(huì)有相同的感受。

1、過多的參數(shù)。

2、輸出參數(shù)。

3、標(biāo)志參數(shù)。

4、死函數(shù)。即永不調(diào)用的方法。

(三)注釋糟糕

1、不恰當(dāng)?shù)男畔ⅰ?/p>

2、廢棄的注釋。

3、冗余注釋。

4、糟糕瞎扯的注釋。

5、注釋掉的代碼。

(四)測(cè)試糟糕

1、測(cè)試不足。可以使用覆蓋率工具,多數(shù)IDE提供了功能。

2、略過小測(cè)試。這個(gè)不能略過。

3、沒有測(cè)試邊界條件

4、只全面測(cè)試相近的缺陷

5、測(cè)試很慢

(五)一般性問題

1、一個(gè)源文件中存在多種語言,如Java、HTML、XML等

2、明顯的行為未被實(shí)現(xiàn)。

3、不正確的邊界行為。

4、忽視安全。

5、重復(fù)。

6、在錯(cuò)誤的抽象層級(jí)上的代碼。如基類和派生類、controller層和service層

7、基類依賴于派生類。

8、信息過多。提供的接口中,需要調(diào)用方調(diào)用的函數(shù)越少越好。

9、死代碼。如在if、catch、工具類中、switch/case中。

10、垂直分隔。變量和函數(shù)應(yīng)該在被使用的地方定義,不應(yīng)該在被使用之處幾百行以外聲明。這種情況在一些巨大的方法中可以見到。

11、前后不一致。命名要前后形式一致。如controller層、service層、dao層同一個(gè)功能的方法名要有一致性。

12、不使用的變量、函數(shù)、沒有信息量的注釋等。這些都可以直接刪除。一般在IDE自動(dòng)生成的代碼中會(huì)看到。

13、耦合。如為了方便,隨意將變量、常量和函數(shù)放在一個(gè)不合適的臨時(shí)地方。

14、特性依戀。類的方法只應(yīng)操作其所屬的變量和函數(shù),不應(yīng)該操作其它類的變量和函數(shù)。但是對(duì)于Controller層直接調(diào)用Service層方法,這種就不算壞味道。像下面這種,從其他類中獲取其他類的變量來進(jìn)行計(jì)算的,這種就是特性依戀了。

// 特性依戀
public class HourlyPayCalculator{
    
    public Money calculateWeeklyPay(HourlyEmployee e){
        int tenthRate = e.getTenthRate().getPennies();
        int tenthsWorked = e.getTenthsWorked();
        ......
        ......
        return new Money();
    }
    
}

// 非特性依戀
public class UserController {
    
    @Autowired
    priavate UserService userService;
    
    public boolean login(String username, String password){
        userService.login(username, password);
    }
}

15、選擇參數(shù)。傳入?yún)?shù)使用:boolean、枚舉元素、整數(shù)或者任何一種用于選擇函數(shù)行為的參數(shù)。

16、晦澀不明的意圖。如使用聯(lián)排表達(dá)式、匈牙利語標(biāo)記法和魔法數(shù)等。

17、位置錯(cuò)誤的權(quán)責(zé)。代碼放錯(cuò)位置,如放到了不同模塊、無關(guān)的類中。

18、不恰當(dāng)?shù)撵o態(tài)方法。有些類是會(huì)用到多態(tài)的,就不要用靜態(tài)方法。

19、使用解釋性變量。在計(jì)算過程中,如果直接計(jì)算 會(huì)很難讀懂計(jì)算過程。此時(shí),加上一些解釋性變量,把計(jì)算過程打散成一些了良好命名的中間值,這樣計(jì)算過程會(huì)易讀很多。

Matcher match = headerPattern.matcher(line);
if(match.find()){
    String key = match.group(1);
    String value = match.group(2);
    headers.put(key.toLowerCase(), value);
}

20、函數(shù)名稱應(yīng)該表達(dá)其行為。

21、理解算法。很多糟糕代碼是因?yàn)槿藗儧]有花時(shí)間去理解算法,而是不停地加if語句和標(biāo)志。

22、把邏輯依賴改為物理依賴。

23、用多態(tài)替代if/else和switch/case

24、遵循標(biāo)準(zhǔn)約定。團(tuán)隊(duì)成員要遵循團(tuán)隊(duì)共同制定的規(guī)范。

25、用命名常量替代魔法數(shù)。

26、準(zhǔn)確。如不能用浮點(diǎn)數(shù)表示貨幣,數(shù)據(jù)庫的查詢不一定返回唯一一條記錄、有并發(fā)更新時(shí)要適當(dāng)加鎖、是否判空、異常是否處理等等。

27、結(jié)構(gòu)比約定好。如使用IDE的強(qiáng)制性結(jié)構(gòu)提示、使用基類使得具體類必須實(shí)現(xiàn)所有方法。

28、封裝條件。

// 糟糕代碼
if (timer.hasExpired() && !timer.isRecurrent()){}

// 優(yōu)雅代碼
if (shouldBeDeleted(timer)){}

29、避免否定性條件。

// 糟糕代碼
if (!buffer.shouldNotCompact()){}

// 優(yōu)雅代碼
if (buffer.shouldCompact()){}

30、函數(shù)只做一件事。

31、掩蓋時(shí)序耦合。這個(gè)見仁見智了。見《代碼整潔之道》p284-285。

32、別隨意。

33、封裝邊界條件。

34、函數(shù)中的語句應(yīng)該只在一個(gè)抽象層級(jí)上。

35、在較高層級(jí)放置可配置數(shù)據(jù)。

public class Arguments {
    public static final String DEFAULT_PATH = "" ;
    public static final String DEFAULT_ROOT = "FitNesseRoot";
    public static final String DEFAULT_PORT = 80 ;
    public static final String DEFAULT_VERSION_DAYS = 14 ;
    
    public void parseCommandLine(String[] args){
        // user 80 by default  這里就不需要寫了。因?yàn)橐呀?jīng)在上面寫好了配置數(shù)據(jù)。
        if(arguments.port == 0) {}
    }
    
}

public class Main {
    public static void main(String[] args){
        Arguments arguments = parseCommandLine(args);
        ......
    }
}

36、避免傳遞瀏覽。不要出現(xiàn)a.getB().getC()。

(六)Java

1、過長(zhǎng)的導(dǎo)入清單。可使用通配符來避免。如import package.*;

2、繼承常量。不能為了使用常量而繼承有常量的類,正確的做法是導(dǎo)入該常量類。

3、常量 VS 枚舉,建議使用枚舉enum。

(七)環(huán)境

1、構(gòu)建項(xiàng)目要很多步驟。

如項(xiàng)目檢出,導(dǎo)入IDE后,還需要四處找額外的jar、xml文件和其它系統(tǒng)需要的雜物。

正常情況,只需要三步:源代碼控制系統(tǒng)檢出項(xiàng)目、導(dǎo)入項(xiàng)目到IDE、直接構(gòu)建項(xiàng)目。

2、進(jìn)行測(cè)試要很多步驟。

最好的是一個(gè)指令就可以執(zhí)行所有測(cè)試。

使用Maven工具,可以快速構(gòu)建、管理項(xiàng)目。

三、編碼時(shí)

(一)命名

1、范圍

變量、函數(shù)、參數(shù)、類、包、目錄、jar文件、war文件、war文件等。

2、要有含義

(1)對(duì)于所有的命名,都要有具體含義,也就是說看到命名就知道是干什么的。這個(gè)對(duì)英語能力有一定的要求。英語不行的可以借助翻譯工具。

(2)不能使用a、b、c、i、j、x、y等進(jìn)行命名。

(3)不要使用魔法數(shù),如直接使用1、2、3、4。

3、不能有誤導(dǎo)

(1)不能使用一些專有名稱。

(2)別用userList,即使真的是List類型,建議也別再名稱中寫出容器類型名稱。如直接用users更好。

(3)注意不要使用不同之處較小的名稱。起碼要有兩個(gè)單詞不同。

(4)不能使用小寫字母“l(fā)”和大寫字母“O”,因?yàn)檫@個(gè)兩個(gè)看起來很像數(shù)字”1“和”0“。

4、要有區(qū)分

(1)不能以數(shù)字系列命名來區(qū)分,如a1、a2、......aN,

// 糟糕命名
public static void copyChars(char[] a1, char[] a2){}
// 優(yōu)雅命名
public static void copyChars(char[] source, char[] destination){}

(2)不要用廢話命名。

廢話一:使用意思沒區(qū)別的命名。如下面這3個(gè)類的名稱雖然不同,但是意思卻沒有區(qū)別。

Product.java
ProductInfo.java
ProductData.java

廢話二:命名帶上類型。

String name
String nameString ;

Customer.java
CustomerObject.java

數(shù)據(jù)表名:
user
user_table

5、不要用縮寫,要能讀

// 糟糕命名
class DtaRcrd102{
    private Date genymdhms;
    private Date modymdhms;
    private final String pszqint = "102" ;     
}

// 優(yōu)雅命名
class Customer{
    private Date generationTimestamp;
    private Date modificationTimestamp;
    private final String recordId = "102" ;     
}

6、要可搜索

不要使用單字母名稱和數(shù)字常量,因?yàn)楹茈y搜索。如數(shù)字1、2、3和字母i、j、e等,很難找出來。

所以,長(zhǎng)名稱比短名稱好。

// 糟糕命名
int[] a = ... ;
int s = 0 ;
for (int j = 0; j<10; j++){
    s += (a[j] * 4)/5 ;
}

// 優(yōu)雅命名
int[] taskEstimate = ... ;
int realDayPerIdealDay = 4 ;
int WORK_DAYS_PER_WEEK = 5 ;
int sum = 0 ;
for (int j = 0;j<NUMBER_OF_TASKS; j++){
    int realTaskDays = taskEstimate[j] * realDayPerIdealDay ;
    int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
    sum += realTaskWeeks ;
}

在該例子中,搜索“sum”比搜索“s”容易多,搜索“WORK_DAYS_PER_WEEK”比搜索“5”容易多。

當(dāng)然,名稱長(zhǎng)短要與其作用域大小相對(duì)應(yīng)。對(duì)于局部變量,名稱短一點(diǎn)可以,但也要可讀;對(duì)于可能被多處代碼使用的、經(jīng)常需要搜索的變量或常量,需要使用適當(dāng)長(zhǎng)的、便于搜索的名稱。單字母名稱只能用于短方法中局部變量。

7、不能使用編碼

(1)不使用匈牙利語標(biāo)記法。Java是強(qiáng)類型的語言,IDE工具在編譯開始前就能偵測(cè)到數(shù)據(jù)類型錯(cuò)誤,所以這種方式在Java開發(fā)中基本沒人使用。這里就不贅述。

(2)不要再使用前綴(如m_)或者后綴。而是應(yīng)當(dāng)把類和函數(shù)寫得足夠小。因?yàn)楝F(xiàn)在很多IDE能夠用顏色區(qū)分成員變量。

// 糟糕命名
public class Part{
    private String m_dsc ;
    public void setName(String name){
        m_desc  = name ;
    }
}

// 優(yōu)雅命名
public class Part{
    private String description ;
    public void setName(String description){
        this.description  = description ;
    }
}

(3)接口和實(shí)現(xiàn)類。接口不要再使用前導(dǎo)字母“I”。

// 糟糕命名
IUserService.java
UserService.java

// 優(yōu)雅命名
UserService.java
UserServiceImpl.java

8、類名和對(duì)象名:名詞或名詞短語

// 糟糕命名
Manage.java
Process.java
Data.java
Info.java

// 優(yōu)雅命名
Customer.java
User.java
Account.java
WikiPage.java
AdressParser.java

類名不能是動(dòng)詞

9、方法名:動(dòng)詞或動(dòng)詞短語

(1)方法名要使用動(dòng)詞或動(dòng)詞短語

// 使用動(dòng)詞或動(dòng)詞短語
postPayment();
deletePage();
savaPage();

(2)屬性訪問器、修改器和斷言(isXXX)

// 屬性訪問器、修改器和斷言應(yīng)根據(jù)其值命名,并按照J(rèn)avaBean標(biāo)準(zhǔn)加上set、get、is前綴。如Employee.java中有一個(gè)屬性name,則屬性訪問器和修改器為setName(String name)和getName()。
String name  = employee.getName();
customer.setName("dave");
if(paycheck.isPosted()){......}

(3)重載構(gòu)造方法

// 重載構(gòu)造方法時(shí),使用描述了參數(shù)的靜態(tài)工廠方法名。如
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
// 通常好于
Complex fulcrumPoint = new Complex(23.0);
// 同時(shí)可以考慮將構(gòu)造方法設(shè)置為private,強(qiáng)制使用這種靜態(tài)工廠方法名。

10、命名不能用笑話、俗語等

// 糟糕命名
// 劈砍
whack();
// 去死吧
eatMyShorts();

// 優(yōu)雅命名
kill();
abort();

11、不能將同一單詞用于不同目的

在多個(gè)類中都有add方法,作用都是通過增加或連接兩個(gè)現(xiàn)存值來獲得新值,相當(dāng)于“+”。

如果要寫一個(gè)新類,該類中有個(gè)方法,作用是將一個(gè)參數(shù)插入到一個(gè)集合中。這個(gè)時(shí)候,是不能再把方法定義為add,因?yàn)檎Z義是不同的,應(yīng)該使用insert或者append等詞命名。

12、使用解決方案領(lǐng)域的名稱

盡量使用計(jì)算機(jī)科學(xué)的術(shù)語、算法名、模式名和數(shù)學(xué)術(shù)語等,取一個(gè)技術(shù)性的名稱。

// 設(shè)計(jì)模式:訪問者模式
AccountVisitor.java

// 框架:任務(wù)隊(duì)列
JobQueue.java

13、使用所涉及問題領(lǐng)域的名稱

如果無法做到12中的用程序員熟悉的術(shù)語來命名,可以考慮采用從所涉及問題領(lǐng)域而來的名稱。

這里不是很理解,本人的想法是,從技術(shù)解決方案領(lǐng)域無法找到合適命名,則從業(yè)務(wù)問題領(lǐng)域入手。

不過,一般不會(huì)出先這種情況。

14、添加有意義的語境

對(duì)于一些變量,如果單獨(dú)寫在一大段代碼中,沒有一個(gè)簡(jiǎn)單明了的語境,是很難讀懂這些變量的。這個(gè)時(shí)候,就需要用類、函數(shù)或者名稱空間來處理這些變量。

(1)使用名稱空間(即前綴)。但不是最優(yōu)方案,不提倡。更好的方案是創(chuàng)建一個(gè)類。

// 糟糕命名
void excute(){
    ......
    ......
    String firstName ;
    String lastName ;
    String street ;
    String houseNumber ;
    String city ;
    String state ;
    String zipcode ;
    ......
    ......
}

// 稍微好一點(diǎn)點(diǎn)的命名
void excute(){
    ......
    ......
    String addrFirstName ;
    String addrLastName ;
    String addrStreet ;
    String addrHouseNumber ;
    String addrCity ;
    String addrState ;
    String addrZipcode ;
    ......
    ......
}


// 優(yōu)雅命名
void excute(){
    ......
    ......
    Address address = new Address();
    ......
    ......
}

class Address {
    private String firstName ;
    private String lastName ;
    private String street ;
    private String houseNumber ;
    private String city ;
    private String state ;
    private String zipcode ;
    // setter和getter方法
} 

(2)案例

// 糟糕命名
public class Main {
    public static void main(String[] args){
        Main name = new Main();
        name.printGuessStatistics('d',2);
    }
    private void printGuessStatistics(char candidate,int count){
        String number ;
        String verb ;
        String pluralModifier ;

        if (count == 0){
            number = "no" ;
            verb = "are" ;
            pluralModifier = "s";
        }else if (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
        );
        System.out.println(guessMessage);
    }
}


// 優(yōu)雅命名
public class Main {
    public static void main(String[] args){
        Main name = new Main();
        name.printGuessStatistics('d',2);
    }

    private void printGuessStatistics(char candidate,int count){
        GuessStatisticsMessage guessStatisticsMessage = new GuessStatisticsMessage();
        String guessMessage =  guessStatisticsMessage.make(candidate,count);

        System.out.println(guessMessage);
    }
}

public class GuessStatisticsMessage {

    private String number ;
    private String verb ;
    private String pluralModifier ;

    public String make(char candidate,int count){
        createPluralDepentMessageParts(count);
        return String.format(
                "There %s %s %s%s",verb,number,candidate,pluralModifier
        );

    }

    private void createPluralDepentMessageParts(int count){
        if (count == 0){
            thereAreNoLetters();
        }else if (count == 1){
            thereIsOneLetters();
        }else {
            thereAreManyLetters(count);
        }
    }

    private void thereAreManyLetters(int count){
        number = Integer.toString(count) ;
        verb = "are" ;
        pluralModifier = "s";
    }

    private void thereIsOneLetters(){
        number = "1" ;
        verb = "is" ;
        pluralModifier = "";
    }

    private void thereAreNoLetters(){
        number = "no" ;
        verb = "are" ;
        pluralModifier = "s";
    }
}

15、不添加沒有意義的語境

命名時(shí)不需要加一些無關(guān)緊要的前綴。如項(xiàng)目的名稱叫做(Online School),那么不需要在每個(gè)類前加上“OS”前綴,如OSUSer.java 、OSAccount.java等

只要短名稱已經(jīng)最夠說清楚,就不需要加長(zhǎng)名稱。

(二)函數(shù)(方法)

1、短小

(1)函數(shù)的第一規(guī)則是要短小,第二條規(guī)則是要更短小。

(2)函數(shù)的函數(shù)不能超過20行。顯示器一屏要能夠看完一個(gè)函數(shù)。

(3)if語句、else if語句、else語句和while語句,其中的代碼只能有一行,這一行一般是函數(shù)調(diào)用語句。

2、只做一件事情

(1)判斷一個(gè)函數(shù)是夠做了多件事情:一是看是否存在不同的抽象層級(jí);二是看能否再拆出一個(gè)函數(shù)。

(2)一個(gè)函數(shù)只能做一件事情。

3、每個(gè)函數(shù)一個(gè)抽象層級(jí)

MVC代表了不同的抽象層級(jí)。從頁面、控制層、服務(wù)層、數(shù)據(jù)層,每一層的方法只能處理該層抽象層級(jí)的事,不能處理其他層級(jí)的事。舉個(gè)例子,數(shù)據(jù)層不能出現(xiàn)調(diào)用服務(wù)層的代碼,在控制層也不能直接出現(xiàn)調(diào)用數(shù)據(jù)層的代碼。

4、switch語句

(1)一般不會(huì)用switch語句。

(2)實(shí)在無法避免了,使用多態(tài)來實(shí)現(xiàn)。這里不贅述,詳見《代碼整潔之道》P35頁。

5、使用描述性的名稱

(1)函數(shù)越短小、功能越集中,就越便于取個(gè)好名稱。所以在發(fā)現(xiàn)很難給函數(shù)取名時(shí),看看函數(shù)是否做了多件事情。

(2)可以使用長(zhǎng)名稱。

(3)命名格式要保持一致,使用與模塊名一脈相承的相關(guān)動(dòng)詞和動(dòng)詞短語給函數(shù)命名。

6、參數(shù)

參數(shù)個(gè)數(shù)不能超過3個(gè),超過的要進(jìn)行封裝。最理想的是沒有參數(shù),第二好是一個(gè)參數(shù)。

通過參數(shù)傳入數(shù)據(jù),但不能通過參數(shù)傳出處理結(jié)果,而是要通過返回值輸出處理結(jié)果。

(1)一個(gè)參數(shù)

傳入單個(gè)參數(shù)有兩種極普遍的理由,一是問關(guān)于這個(gè)參數(shù)的問題,如boolean fileExists("MyFile") ,問這個(gè)路徑的文件是否存在;二是操作這個(gè)參數(shù),將其轉(zhuǎn)為其他東西, 如InputStream fileOpen("MyFile"),把String類型的文件名轉(zhuǎn)換為InputStream類型的返回值。

還有一種不是很普遍的理由,那就是事件:有輸入?yún)?shù),無輸出參數(shù)。如void passwordAttemptFailedNtimes(int attempts)。要小心使用這種形式。

以上這三種形式都是較好的。

盡量避免編寫不遵循這些形式的一元函數(shù),如使用輸出參數(shù)而不是返回值。如

// 糟糕用法
void transform(StringBuffer out);
// 優(yōu)雅用法
StringBuffer transform(StringBuffer in);

(2)標(biāo)志參數(shù)

不要使用標(biāo)志參數(shù),這種參數(shù)丑陋不堪。如向函數(shù)傳入布爾值,簡(jiǎn)直駭人聽聞。見《代碼整潔之道》p46頁的代碼清單3-7例子。

// 丑陋代碼
render(Boolean isSuite);

// 優(yōu)雅代碼
renderForSuite();
renderForSingleTest();

(3)兩個(gè)參數(shù)

二元函數(shù)容易搞錯(cuò)兩個(gè)參數(shù)的順序。如assertEquals(expected,actual),容易搞錯(cuò)expected與actual位置

盡量利用一些機(jī)制將其換成一元函數(shù):

writeField(outputStream,name);

方案一:可以將writeField方法寫成outputStream的方法,則直接用outputStream.writeField(name);
方案二:把outputStream寫成當(dāng)前類的成員變量,從而無需再傳遞;
方案三:分離出FiledWriter的新類,在其構(gòu)造器中采用outputStream,并且包含write方法。

(4)三個(gè)參數(shù)

三元函數(shù)更加容易搞錯(cuò)三個(gè)參數(shù)的順序。如assertEquals(message, expected, actual),很容易將message誤以為expected。

因此這里需要注意。

(5)參數(shù)對(duì)象

如果函數(shù)看起來需要兩個(gè)、三個(gè)或三個(gè)以上參數(shù),則說明其中一些參數(shù)需要封裝為類。如:

Circle makeCircle(double x,double y,double radius);
Circle makeCircle(Piont center,double radius);

(6)參數(shù)列表

這里參數(shù)列表指的是可變參數(shù)。如String.format方法。

public String format(String format,Object... args);

有可變參數(shù)的函數(shù)可能是一元、二元和三元的,不要超過這個(gè)數(shù)量。

void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int out, Integer... args);

7、函數(shù)偷偷多做事

函數(shù)名表明只做了一件事,但是又偷偷地做了其它事情,這些事情包括:一,對(duì)自己類中的變量做出未能預(yù)期的改動(dòng),二是把變量搞成向函數(shù)傳遞的參數(shù)或者系統(tǒng)全局變量。這兩種情況都會(huì)導(dǎo)致時(shí)序性耦合與順序依賴。

public class UserValidator {
    
    private Cryptographer cryptographer ;
    
    public boolean checkPassword(String username,String password){
        User user = UserGateway.findByName(username);
        if (user != User.NULL){
            String codedPhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codedPhrase,password);
            if ("Valid Password".equals(phrase)){
                Session.initialize();
                return true ;
            }
        }
        return false ;
    }
    
}

checkPassword()方法,顧名思義,就是用來檢查密碼的。這個(gè)名稱并沒有說明會(huì)初始化該次會(huì)話,但是在代碼中卻調(diào)用了Session.initialize();,也就是說,調(diào)用該方法來檢查密碼時(shí),會(huì)刪除現(xiàn)有會(huì)話。這就造成了時(shí)序性耦合。

所以checkPassword()方法需要重新命名為checkPasswordAndInitializeSession()方法。當(dāng)然這樣會(huì)違背了只做一件事的原則。

因此,最終方案是要將Session.initialize();提取出來。

8、分割指令和詢問

函數(shù)要么做什么事,要么回答什么事,二者不可兼得。

函數(shù)要么修改某對(duì)象的狀態(tài),要么返回該對(duì)象的有關(guān)信息,二者不能同時(shí)做。

public boolean set(String attibute, String value){}

public static void main(String[] args){
    if(set("username","unclebob"){
        return true ;
    }
    return false ;
}

在讀者看來,會(huì)存在疑問:這個(gè)方法時(shí)在問username屬性值是否之前已設(shè)置為unclebob,還是在問username屬性值是否成功設(shè)置為unclebob呢?這里很難判斷其含義。

解決方案是將指令和詢問分隔開,

if(attributeExists("username")){
    setAttribute("username","unclebob");
}

這樣,看起來就很明顯知道:如果username存在,則將username屬性值設(shè)置為unclebob。

9、使用異常替代返回錯(cuò)誤碼

(1)抽離try/catch代碼塊

返回錯(cuò)誤碼,是在要求調(diào)用者立刻處理錯(cuò)誤。

if(deletePage(page) == E_OK){
    if(registry.deleteReference(page.name) == E_OK){
        if(configKeys.deleteKey(page.name.makeKey())==E_OK){
            logger.log("page deleted");
        }else{
            logger.log("configKey not deleted");
        }
    }else{
        logger.log("deleteReference from registry failed");
    }
}else{
    logger.log("delete failed");
    return E_ERROR;
}

如果使用異常代替返回錯(cuò)誤碼,可簡(jiǎn)化為

try{
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey();
}catch(Exception e){
    logger.log(e.getMessage());
}

但是,try/catch語句很丑,搞亂了代碼結(jié)構(gòu),把錯(cuò)誤處理與正常流程混在一起。所以,要把try和catch代碼塊的主體部分抽離,另外形成函數(shù)。

public void delete(Page page){
    try{
        deletePageAndAllReferences(page);
    }catch(Exception e){
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception{
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey(); 
}

private void logError(Exception e){
    logger.log(e.getMessage());
}

delete函數(shù)只和錯(cuò)誤處理有關(guān)

deletePageAndAllReferences函數(shù)只處理業(yè)務(wù),與錯(cuò)誤處理無關(guān)了。

(2)錯(cuò)誤處理就是一件事。函數(shù)處理錯(cuò)誤,就是做一件事。也就是說, 函數(shù)如果是處理異常的,關(guān)鍵字try就是這個(gè)函數(shù)的第一個(gè)單詞,而且在catch/finally代碼塊后面不應(yīng)該有其它代碼了。

(3)Error.java依賴磁鐵

使用返回錯(cuò)誤碼,一般是某個(gè)類或者枚舉。這個(gè)類就像一顆依賴磁鐵:其它許多類都導(dǎo)入和使用它。當(dāng)Error枚舉修改時(shí),所有其他類都要重新編譯和部署。

public enum Error{
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_EVENT;
}

使用異常替代返回錯(cuò)誤碼,新異常可以從異常類派生出來,無需重新編譯和部署。

10、不要重復(fù)

重復(fù)是軟件中邪惡的根源。很過原則和實(shí)踐規(guī)則都是為了控制與消除重復(fù)的。如數(shù)據(jù)庫范式是為了消除數(shù)據(jù)重復(fù),面向?qū)ο缶幊虒⒋a集中到基類,避免代碼重復(fù)。面向切面、面向組件編程等,也是消除重復(fù)的策略。

11、結(jié)構(gòu)化編程

結(jié)構(gòu)化編程規(guī)則:每個(gè)函數(shù)、函數(shù)中的每個(gè)代碼塊都應(yīng)該有一個(gè)入口、一個(gè)出口,意味著,每個(gè)函數(shù)只有一個(gè)return語句,循環(huán)中不能有break和continue,不能有任何的goto語句。

小函數(shù):一般不需要遵守,助益不大。因?yàn)槎际菍懶『瘮?shù),所以這個(gè)可以略過。

大函數(shù):結(jié)構(gòu)化編程有明顯好處。

12、如何寫出這樣的函數(shù)

(1)一開始時(shí),可能會(huì)相對(duì)長(zhǎng)和復(fù)雜。有太多縮進(jìn)和嵌套循環(huán)。有過長(zhǎng)的參數(shù)列表。名稱比較隨意,也會(huì)有部分重復(fù)代碼。

(2)寫單元測(cè)試代碼,覆蓋每行丑陋的代碼。

(3)分解函數(shù)、修改名稱、消除重復(fù)。縮短和重新安置方法,有時(shí)還會(huì)拆散類。同時(shí)要保持測(cè)試通過。

(4)遵循以上的規(guī)則,組裝好函數(shù)。

有個(gè)小技巧:如果發(fā)現(xiàn)某個(gè)方法不能進(jìn)行簡(jiǎn)單的單元測(cè)試,那么這個(gè)方法肯定有問題。

(三)注釋

1、注釋不能美化糟糕的代碼

別給糟糕的代碼寫注釋,直接重寫。

2、好注釋

(1)法律信息

(2)提供信息的注釋。如解釋某個(gè)抽象方法的返回值。

(3)對(duì)意圖的解釋。如程序員解釋盡力做了優(yōu)化后,仍然這樣寫的原因。

(4)闡釋。如把某些晦澀難懂的參數(shù)或返回值的意義翻譯為某種可讀形式。

(5)警示。警告其他程序員出現(xiàn)某種后果的注釋。如警示一些單元測(cè)試不能跑的(Junit4測(cè)試框架可以使用@Ignore注解)、日期工具類中提醒SimpleDateFormat是線程不安全的,所以需要每次都實(shí)例化對(duì)象等。

(6)TODO注釋。不過需要定期清理。

(7)放大。如使用“非常重要”等字眼,提醒其他程序員注意該處代碼。

(8)公共API的Javadoc。這個(gè)不能缺了,但是一定要準(zhǔn)確。

3、壞注釋

(1)喃喃自語。程序員的自我表達(dá),只有作者自己看得懂。

(2)多余的注釋。代碼已經(jīng)能清晰說明了,但還是加上了無關(guān)緊要的注釋。一般出現(xiàn)在一些自動(dòng)生成或者復(fù)制粘貼的模板代碼中。注意要?jiǎng)h除這類注釋。

(3)誤導(dǎo)性注釋。如那些不夠精確的注釋,會(huì)誤導(dǎo)讀者。

(4)循規(guī)式注釋。不一定每個(gè)函數(shù)每個(gè)變量都要有注釋。但是本人覺得業(yè)務(wù)代碼基本都要寫。

(5)日志式注釋。每次修改代碼都加一條注釋,這種注釋是不需要的。

(6)廢話注釋。一般是IDE工具自動(dòng)生成的注釋,沒啥用。

(7)可怕的廢話。一般是復(fù)制粘貼過來的廢話注釋,廢話的廢話。

(8)位置標(biāo)記。這種沒有必要。

//////////////////////////////////////////////////////
......
......
/////////////////////////////////////////////////////

(9)括號(hào)后的注釋。

try{
    while(){
        
    } // while
} //try
catch{
    
} //catch

(10)歸屬和署名。源代碼控制系統(tǒng)(Git和SVN)才是這些信息最好的歸屬地。

/* Added by Dave */
/* Modified by Dave */

(11)注釋掉的代碼。刪掉吧,有源代碼控制系統(tǒng),怕啥。

(12)HTML注釋。不要寫。

(13)非本地信息。要寫當(dāng)前位置的注釋,不要寫遠(yuǎn)在他方的注釋。

(14)信息過多。不要寫一大堆注釋,要言簡(jiǎn)意賅。

(15)不明顯的關(guān)系。注釋要和代碼有關(guān)聯(lián)。

(16)函數(shù)頭。這里建議不寫注釋。但這個(gè)本人覺得在業(yè)務(wù)代碼中,還是建議每個(gè)函數(shù)頭要寫注釋??错?xiàng)目質(zhì)量吧。

(17)非公共代碼中的Javadoc。非公共代碼,就不要寫Javadoc注釋。

(四)格式

1、格式的目的

格式會(huì)影響可讀性??勺x性會(huì)影響可維護(hù)性和擴(kuò)展性。

2、垂直格式

(1)垂直方向上,代碼最頂部應(yīng)該是高層次概念和算法,細(xì)節(jié)往下逐次展開,直到最底層的函數(shù)和細(xì)節(jié)。也就是說,被調(diào)用的函數(shù)要放在調(diào)用函數(shù)的下面。就像看報(bào)紙一樣,從上到下閱讀,頂部是頭條,接著第一段是故事大綱,然后細(xì)節(jié)在后面逐漸展開。

(2)概念間的隔開。適當(dāng)使用一些空白行隔開,如package、import、每一塊成員變量間、每個(gè)方法間、方法內(nèi)每一塊代碼間(因?yàn)槎际切『瘮?shù),所以方法內(nèi)一般不需要空白行)。

(3)概念間的靠近。不要使用空白行、或者不恰當(dāng)?shù)淖⑨尭魯嗑o密相關(guān)的代碼。

// 糟糕代碼
public class ReporterConfig{
    /**
     * The class name of the reporter listener
     */
    private String m_className ;
    
    /**
     * The properties of the reporter listener
     */
    private List<Property> m_properties = new ArrayList<Property>(); 
    
    public void addProperty(Property property){
        m_properties.add(property);
    }
}

// 優(yōu)雅代碼
public class ReporterConfig{

    private String classNameOfReporterListener ;
    private List<Property> propertiesOfReporterListener = new ArrayList<Property>(); 
    
    public void addProperty(Property property){
        propertiesOfReporterListener.add(property);
    }
}

(4)概念間的距離

第一,函數(shù)短小,局部變量應(yīng)該在函數(shù)的頂部出現(xiàn)。

第二,成員變量應(yīng)該在類的頂部出現(xiàn)。

第三,相關(guān)函數(shù)。若一個(gè)函數(shù)調(diào)用了同一個(gè)類的另一個(gè),應(yīng)該把這兩個(gè)放在一起,而且調(diào)用者應(yīng)該盡可能放在被調(diào)用者上面。

第四,概念相關(guān)的代碼應(yīng)該放在一起。如相關(guān)性可能來自于執(zhí)行相似操作的一組函數(shù)。

public class Assert{
    public static void assertTrue();
    public static void assertFalse();
    public static void assertTrue(String message);
    public static void assertFalse(String message);
}

(5)概念間的順序

被調(diào)用的函數(shù)應(yīng)該放在執(zhí)行調(diào)用函數(shù)的下面。這樣,閱讀代碼時(shí),看最前面的幾個(gè)函數(shù)就能大概知道該類主要是做什么的了。

3、橫向格式

一行代碼應(yīng)該有多寬?小屏幕一屏能展示,不用拖動(dòng)滾動(dòng)條到右邊。

(1)隔開。

第一,在賦值操作符兩邊加上空格。

int lineSize = line.length();
totalChars += lineSizw();

第二,不在函數(shù)名和左圓括號(hào)之間加空格。

第三,在函數(shù)括號(hào)中的參數(shù)之間加空格。

public static boolean checkUserNameAndPassword(String username, String password);

(2)對(duì)齊。

Java開發(fā)無需關(guān)注橫向方向上的對(duì)齊,而是要關(guān)注垂直的長(zhǎng)度。如果發(fā)現(xiàn)需要對(duì)齊才好看清楚,那就要思考該類是否需要拆分了。

public class FitNesseExpediter implements ResponseSender{
    
    private Socket socket;
    private InputStream input;
    private OutputStream output;
    private Request request;
    private Response response;
    private FitNesseContext context;
    protected long requestParsingTimeLimit;
    private long requestProcess;
    private long requestParsingDeadline;
    private boolean hasError;
    
    ......
}

(3)縮進(jìn)。一般IDE工具可以自動(dòng)縮進(jìn)。

(4)空范圍。while或for語句的語句體為空時(shí),容易忽略在同一行的分號(hào)";"。

// 后面的分號(hào)容易被忽略
while (dis.read(buf, 0, readBufferSize) != -1);

// 好一點(diǎn)
while (dis.read(buf, 0, readBufferSize) != -1)
;

// 優(yōu)雅代碼
while (dis.read(buf, 0, readBufferSize) != -1){
    
};

4、團(tuán)隊(duì)規(guī)則

在一個(gè)團(tuán)隊(duì)中工作,則需要定一個(gè)團(tuán)隊(duì)規(guī)則,一旦定好,所有人包括后來接手的人都要接受。

(1)啟動(dòng)項(xiàng)目時(shí),團(tuán)隊(duì)要先制定一套編碼規(guī)范,如什么地方放置括號(hào),縮進(jìn)幾個(gè)字符,如何命名類、變量和方法等等。

(2)定好編碼規(guī)范后,將這些規(guī)則編寫今IDE的代碼格式功能。

(3)后面接手的人要一定要按照這種編碼規(guī)范來進(jìn)行編碼。

不要用不同風(fēng)格來編寫同一個(gè)項(xiàng)目的源代碼。

由此可見,編碼規(guī)范一旦定下,就要一直沿用。所以,在項(xiàng)目一開始時(shí),就要非常注重編碼規(guī)范的制定。

(五)對(duì)象與數(shù)據(jù)結(jié)構(gòu)

慎用鏈?zhǔn)秸{(diào)用。這類代碼被稱作火車失事,是一種骯臟的風(fēng)格。

最為精煉的數(shù)據(jù)結(jié)構(gòu),是數(shù)據(jù)傳送對(duì)象,即DTD(Data Transfer Objects),只有公共變量、沒有函數(shù)。

對(duì)象曝露行為,隱藏?cái)?shù)據(jù)。所以便于添加新對(duì)象類型而無需修改既有行為,同時(shí)難以在既有對(duì)象中添加新行為。

數(shù)據(jù)結(jié)構(gòu)曝露數(shù)據(jù),隱藏行為。便于向既有數(shù)據(jù)結(jié)構(gòu)添加新行為,同時(shí)難以向就有函數(shù)添加新數(shù)據(jù)結(jié)構(gòu)。

(六)錯(cuò)誤處理

錯(cuò)誤處理很重要,但是如果它搞亂了代碼邏輯,那就是錯(cuò)誤的做法。

1、使用異常,而不是返回碼

遇到錯(cuò)誤時(shí),最好是拋出一個(gè)異常。見(二)函數(shù)(方法)的第9條。

2、先寫try-catch-finally語句

3、使用不可控異常

對(duì)于一般的應(yīng)用開發(fā),使用不可控異常。

如果使編寫一套關(guān)鍵代碼庫,則可以考慮使用可控異常。

異常分類 說明
可控異常(checked exception) 繼承自java.lang.Exception的異常,這種異常需要顯式的try/catch或throw出來,否則編譯不通過;
不可控異常(unchecked exception) 繼承自java.lang.RunTimeException的異常,這種異常不需要顯式的try/catch或throw出來編譯就能通過。也叫運(yùn)行時(shí)異常

之所以使用不可控異常,是因?yàn)椴豢煽禺惓?梢院?jiǎn)化代碼。然而,由于不需要額外處理就能編譯通過,所以最好在調(diào)用前檢查一下可能發(fā)生的錯(cuò)誤,比如空指針、數(shù)組越界等。

4、catch時(shí)打印異常發(fā)生的環(huán)境

要將失敗的操作和失敗類型等打印記錄下來,便于追蹤排查問題。

使用日志系統(tǒng),傳遞足夠的信息給catch塊,并記錄下來。

這里參考《阿里巴巴Java開發(fā)手冊(cè)》的日志打印規(guī)范。

5、自定義異常類

為某個(gè)功能定義一個(gè)異常類型,可以簡(jiǎn)化代碼。

// 糟糕代碼。catch里面代碼大量重復(fù)。
ACMEReport port  = newACMEReport();

try {
    port.open();
} catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log("......",e);
} catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("......",e);
} catch (GMXError e) {
    reportPortError(e);
    logger.log("......",e);
} finally {
    ......
}

// 優(yōu)雅代碼。
LocalPort port = new LocalPort(12);
try {
    port.open();
} catch(PortDeviceFailure e) {
    reportPortError(e);
    logger.log(e.message(),e);
} finally {
    ......
}

// ACMEReport封裝進(jìn)LocalPort
public class LocalPort {
    private ACMEReport innerPort;
    
    public LocalPort(int portNumber){
        innerPort = new ACMEReport(portNumber);
    }
    
    public void open(){
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e){
            throw new PortDeviceFailure(e);
        } catch (GMXError e){
            throw new PortDeviceFailure(e);
        }finally {
            ......
        }
    }

}

// 自定義異常類
class PortDeviceFailure extends RunTimeException {}

6、定義常規(guī)流程

特例模式。

該情況很少見,不展開說了。具體見《代碼整潔之道》p100-p101。

7、不要返回null值

新寫的代碼,不要返回null值。

調(diào)用第三方API返回null值的方法,要在新方法中打包這個(gè)方法,在新方法中拋出異?;蛘叻祷靥亓袑?duì)象。

8、不要傳遞null值

準(zhǔn)確講,是禁止傳入null值。

(七)邊界

邊界代碼一般指函數(shù)的入?yún)⒑头祷刂?、第三方API的規(guī)范。

1、邊界不用Map

不要使用Map作為傳入?yún)?shù)類型;不要使用Map作為返回值類型

可以將Map進(jìn)行包裝后再使用。

// 糟糕代碼:直接將Map作為參數(shù)在系統(tǒng)中傳遞
Map sensors = new  HashMaps();
......
Sensor sensor = (Sensor)sensors.get(sensorId);

// 使用泛型,好一點(diǎn)
Map<Sensor> sensors = new  HashMaps<Sensor>();
......
Sensor sensor = sensors.get(sensorId);

// 將Map包裝起來,在系統(tǒng)中傳遞的是包裝類Sensors
public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(String id){
        return (Sensor)sensors.get(id);
    }
    
    ......
}

Sensors sensors = getSensors();
......
Sensor sensor = sensors.getById(id);

2、識(shí)別應(yīng)用和第三方API的邊界接口

如學(xué)習(xí)log4j框架時(shí),要能夠?qū)W會(huì)將應(yīng)用程序的其它部分與log4j的邊界接口隔離開。

3、對(duì)接的API還沒設(shè)計(jì)出來時(shí)

這種方式一般不建議的,除非是真的無法避免了。

簡(jiǎn)單來說,就是兩個(gè)系統(tǒng)之間交互的API格式還沒定好,有一方b的進(jìn)度是延后的,超前的團(tuán)隊(duì)a也不可能等待,所以a會(huì)先單方面定義好一個(gè)API進(jìn)行開發(fā)。等b進(jìn)度趕上,和a共同制定了最終的API,此時(shí)a在編寫一個(gè)適配器類來對(duì)接兩個(gè)接口。這是一種適配器模式,是屬于事后的補(bǔ)救措施??梢詤⒖肌对O(shè)計(jì)模式之禪》p215的第19章《適配器模式》

4、小結(jié):整潔的邊界

(1)整潔的邊界要考慮到需要改動(dòng)時(shí),邊界不需要太大代價(jià)來修改,甚至重寫。

(2)邊界上的代碼需要清晰的分割和定義期望的測(cè)試。學(xué)會(huì)進(jìn)行學(xué)習(xí)型測(cè)試來理解第三方代碼,找出邊界。

(3)避免我們的代碼過多了解第三方代碼中特定信息

(4)邊界傳值:兩種方法,包裝和使用適配器模式。

(八)單元測(cè)試

測(cè)試驅(qū)動(dòng)開發(fā)。

1、TDD三定律

(1)在編寫不能通過的單元測(cè)試前,不可編寫生產(chǎn)代碼

(2)只可編寫剛好無法通過的單元測(cè)試,不能編譯也算不通過

(3)只可編寫剛好足以通過當(dāng)前失敗測(cè)試的生產(chǎn)代碼。

2、保持測(cè)試的整潔

(1)臟測(cè)試等于沒有測(cè)試,甚至比沒有測(cè)試更壞。

(2)測(cè)試代碼和生產(chǎn)代碼一樣重要。

(3)單元測(cè)試可以讓代碼可擴(kuò)展、可維護(hù)、可復(fù)用。

3、整潔的測(cè)試

可讀性。其實(shí)和生產(chǎn)代碼的編碼規(guī)范差不多。測(cè)試代碼可按照這三個(gè)環(huán)節(jié)來寫:

第一是構(gòu)造測(cè)試數(shù)據(jù);

第二是操作測(cè)試數(shù)據(jù)。這一部分往往就是生產(chǎn)代碼;

第三是檢驗(yàn)操作是否得到期望結(jié)果。

(1)測(cè)試代碼要有一定的流程規(guī)范,如上面提到的三個(gè)環(huán)節(jié)。

(2)測(cè)試和生產(chǎn)可以有雙重標(biāo)準(zhǔn)。但是測(cè)試代碼一定整潔。測(cè)試代碼和和生產(chǎn)代碼的不同應(yīng)該是在內(nèi)存和CPU效率,而不是整潔方面,兩者都要整潔。

4、每個(gè)測(cè)試一個(gè)斷言

單個(gè)測(cè)試中的斷言數(shù)量應(yīng)該最小化。

每個(gè)測(cè)試函數(shù)只測(cè)試一個(gè)概念。

5、F.I.R.S.T

整潔測(cè)試遵循以下5條規(guī)則:

Fast:快速。測(cè)試運(yùn)行要快。如果發(fā)現(xiàn)測(cè)試很慢,就要懷疑是不是性能問題了。

Independent:獨(dú)立。各個(gè)測(cè)試之間要互相獨(dú)立。

Repeatable:可重復(fù)。測(cè)試要能夠在任何環(huán)境中重復(fù)通過。

Self-Validation:自足驗(yàn)證。測(cè)試要有布爾值輸出,不能手工對(duì)比來確認(rèn)測(cè)試是否通過,應(yīng)使用assert方法。

Timely:及時(shí)。測(cè)試應(yīng)及時(shí)編寫。單元測(cè)試代碼要在使其通過的生產(chǎn)代碼之前編寫。

(九)類設(shè)計(jì)

前面一直講的是如何編寫好的代碼行和代碼塊。如函數(shù)的恰當(dāng)構(gòu)造,函數(shù)之間如何互相關(guān)聯(lián)等。

現(xiàn)在將注意力放到代碼組織的更高層面,即類,來探討如何得到整潔代碼。

更加具體的可以參考《設(shè)計(jì)模式之禪》

1、類的組織

(1)順序:

第一,公共靜態(tài)常量

第二,私有靜態(tài)變量

第三,私有成員變量

第四,公共函數(shù)

第五,私有函數(shù)

以下例子僅為了說明類的組織順序,其命名是不正確的。

public class Main {
    public static final String SALT = "salt" ;
    
    private static final String SALT = "salt" ;

    private String String username ;
    
    Main(){}
    
    public void f(){
        a();
        b();
    }
    
    private void a(){};
    private void b(){};
    
    public void g(){
        c();
    }
    
    private void c(){};
    
    ......
}

2、類要短小

類要短小,更加短小。

衡量函數(shù)是通過計(jì)算代碼行數(shù)衡量大??;衡量類,采用計(jì)算權(quán)責(zé)衡量。

類的名稱應(yīng)當(dāng)可以描述其權(quán)責(zé)。如果無法給一個(gè)類定一個(gè)精確的名稱,那這個(gè)類就太長(zhǎng)了。類名越含糊,改類就擁有過多權(quán)責(zé)。大概25個(gè)字母能描述一個(gè)類,且不能出現(xiàn)“if”、“and”、“or”、“but”等。

(1)單一權(quán)責(zé)原則

單一權(quán)責(zé)原則(SRP)認(rèn)為,類或模塊只有一條修改的理由。

系統(tǒng)應(yīng)該有許多選小的類組成,而不是由少量巨大的類組成。

(2)內(nèi)聚

類應(yīng)只有少量實(shí)體變量。

類中的每個(gè)方法都應(yīng)該操作一個(gè)或多個(gè)實(shí)體變量。

通常而言,方法操作的變量越多,就越內(nèi)聚到類上。

如果一個(gè)類中每個(gè)變量都被每個(gè)方法所使用,那該類具有最大的內(nèi)聚性。

一般來說,一個(gè)類的要有較高的內(nèi)聚性。

如果發(fā)現(xiàn),一個(gè)類中有的方法沒有引用改類的任何變量,也沒有操作改類的任何變量,那么這個(gè)方法可以剝離出來。

如果發(fā)現(xiàn),一個(gè)類中有的實(shí)體變量沒有被任何方法使用,那這個(gè)變量就可以直接刪除了。

(3)內(nèi)聚會(huì)得到許多短小的類

將大函數(shù)拆成小函數(shù),往往也是將類拆分為多個(gè)小類的時(shí)機(jī)。

3、為了修改而組織

(1)開發(fā)-閉合原則:類應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。

// 一個(gè)必須打開修改的類
public class Sql{
    public Sql(String table, Column[] columns);
    public String create();
    public String insert(Object[] fields);
    public selectAll();
    public findByKey(String keyColumn, String keyValue);
    public select(Column column, String pattern);
    public select(Criteria criteria);
    public preparedInsert();
    private columnList(Column[] columns);
    private valuesList(Object[] fields, final Column[] columns);
    private selectWithCriteria(String criteria);
    private placeholderList(Column[] columns);
    
    // 增加update語句,需要修改這個(gè)類
    public String update(Object[] fields);
}

當(dāng)需要增加一種新語句時(shí),需要修改Sql類。重構(gòu)一下:

// 一組封閉的類
public abstract class Sql{
    public Sql(String table, Column[] columns);
    public abstract String generate();
}

public class CreateSql extends Sql{
    public CreateSql(String table, Column[] columns){}
    @Override
    public String generate(){}
}

public class SelectSql extends Sql{
    public SelectSql(String table, Column[] columns){}
    @Override
    public String generate(){}
}

public class InsertSql extends Sql{
    public InsertSql(String table, Column[] columns, Object[] fields){}
    @Override
    public String generate(){}
    
    private String valuesList(Object[] fields, final Column[] columns){}
}

public class SelectWithCriteriaSql extends Sql{
    public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria){}
    @Override
    public String generate(){}
}

public class SelectWithMatchSql extends Sql{
    public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern){}
    @Override
    public String generate(){}
}

public class FindByKeySql extends Sql{
    public FindByKeySql(String table, Column[] columns, String keyColumn, String keyValue){}
    @Override
    public String generate(){}
}

public class PreparedInsertSql extends Sql{
    public PreparedInsertSql(String table, Column[] columns){}
    @Override
    public String generate(){}
    
    private placeholderList(Column[] columns);
}

public class Where{
    public Where(String criteria){}
    public String generate(){}
}

public class ColumnList(){
    public ColumnList(Column[] columns){}
    public String generate(){}
}

// 此時(shí),增加update語句,不需要修改原來的任何類。只需要新建一個(gè)子類,繼承父類Sql
public class UpdateSql extends Sql{
    public UpdateSql(String table, Column[] columns, Object[] fields){}
    @Override
    public String generate(){}
}

(2)依賴倒置原則:類應(yīng)當(dāng)依賴于抽象,而不是依賴于具體細(xì)節(jié)(如實(shí)現(xiàn)類)。

需求會(huì)變,所以代碼也會(huì)變。

接口(抽象類)是概念,具體類是實(shí)現(xiàn)細(xì)節(jié)。

把變化的東西放到具體類,接口保持定義好后保持不變。

所以,需求變了,只需修改具體類,不用修改接口。

// 接口
public interface StockExchange {
    Money currentPrice(String symbol);
}

// 實(shí)現(xiàn)類
public class FixedStockExchangeSub implements StockExchange{
    
}

// 客戶端調(diào)用
public class Portfolio {
    private StockExchange exchange;
    // 依賴StockExchange接口,而不是具體類
    public Portfolio(StockExchange exchange){
        this.exchange = exchange;
    }
    ......
}

// 測(cè)試
public class portfolioTest {
    private FixedStockExchangeSub exchange ;
    private Portfolio portfolio;
    
    @Before
    protected void setUp() throws Exception(){
        exchange = new FixedStockExchangeSub();
        exchange.fix("MSFT",100);
        portfolio = new Portfolio(exchange);
    }
    
    @Test
    public void GivenFiveMSFTTotalShouldBe500() throws Exception{
        portfolio.add(5,"MSFT");
        Assert.assertEquals(500,portfolio.value());
    }
}

(十)系統(tǒng)

前面講的是類如何得到整潔代碼。

這里討論更高的抽象層級(jí),如何在系統(tǒng)層級(jí)保持整潔。

1、構(gòu)造和使用分開:依賴注入

工廠模式

2、AOP

代理模式、Java AOP框架、AspectJ

3、模塊化

(十一)小結(jié)

4條簡(jiǎn)單的規(guī)則,按優(yōu)先級(jí)從高到低,排列如下:

第一,運(yùn)行所有測(cè)試。緊耦合的代碼難以編寫測(cè)試。

第二,不可重復(fù)。可用模板方法模式消除明顯重復(fù)。

第三,表達(dá)了程序員的意圖。如好的命名、函數(shù)和類短小等

第四,盡可能減少類和方法的數(shù)量。

優(yōu)秀的軟件設(shè)計(jì):提升內(nèi)聚性,降低耦合度,關(guān)注切面,模塊化,縮小函數(shù)和類,使用好名稱等。

四、重構(gòu)時(shí)

這里獨(dú)立成一篇博客來寫。在撰寫中。

代碼整潔之道-理論-重構(gòu)

五、并發(fā)編程

這里獨(dú)立成一篇博客來寫。待撰寫。

代碼整潔之道-理論-并發(fā)編程

六、總結(jié)

代碼質(zhì)量、架構(gòu)和項(xiàng)目管理決定軟件質(zhì)量,代碼質(zhì)量是重要因素。

想成為更好的程序員,基礎(chǔ)是要能寫整潔優(yōu)雅的代碼。

糟糕的代碼會(huì)毀掉一個(gè)項(xiàng)目,甚至毀掉一個(gè)公司。

整潔代碼要立刻動(dòng)手寫,因?yàn)樯院蟮扔谟啦弧?/p>

讓經(jīng)過你手的代碼能夠更干凈些。

七、參考

《代碼整潔之道》

《重構(gòu):改善既有代碼的設(shè)計(jì)》

《設(shè)計(jì)模式之禪》

《Head First設(shè)計(jì)模式》

《阿里巴巴Java開發(fā)手冊(cè)》

《Java并發(fā)編程的藝術(shù)》

《Java并發(fā)編程:核心方法與框架》

《Java程序性能優(yōu)化》

《深入理解Java虛擬機(jī)》

如何寫出優(yōu)雅的代碼

寫代碼時(shí)應(yīng)該注意的問題

八、實(shí)戰(zhàn)

這里獨(dú)立成一篇博客來寫。計(jì)劃撰寫。

代碼整潔之道-實(shí)驗(yàn)-重構(gòu)Args

代碼整潔之道-實(shí)驗(yàn)-重構(gòu)Junit

代碼整潔之道-實(shí)驗(yàn)-重構(gòu)SerialDate

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、整潔代碼 A.混亂的代價(jià) 1.有些團(tuán)隊(duì)在項(xiàng)目初期進(jìn)展迅速,但有那么一兩年的時(shí)間卻慢去蝸行。對(duì)代碼的每次修改都影...
    ZyBlog閱讀 2,269評(píng)論 0 2
  • 代碼整潔之道 Clean Code 第一章 整潔代碼 代碼的重要性我們永遠(yuǎn)拋不掉代碼,因?yàn)榇a呈現(xiàn)了需求的細(xì)節(jié)。在...
    Pengzh1閱讀 1,338評(píng)論 0 1
  • 整潔代碼 Leblanc : Later equals never.(勒布朗法則:稍后等于永不) 對(duì)代碼的每次修改...
    foever_f1eb閱讀 929評(píng)論 0 0
  • 目錄及筆記鏈接 序 丹麥諺語:小處誠實(shí)非小事。 建筑師路德維希:神在細(xì)節(jié)之中。 日本的 5S 哲學(xué): 整理(整理、...
    小鐳Ra閱讀 1,543評(píng)論 3 3
  • [cp]#研途有悟[超話]# 倒計(jì)時(shí)一下子跳到了50天,已經(jīng)記不清去年這個(gè)時(shí)候自己的進(jìn)度。今年最大的感觸就是千萬別...
    行人歌閱讀 308評(píng)論 0 0

友情鏈接更多精彩內(nèi)容