[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ú)立成一篇博客來寫。在撰寫中。
五、并發(fā)編程
這里獨(dú)立成一篇博客來寫。待撰寫。
六、總結(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ī)》
八、實(shí)戰(zhàn)
這里獨(dú)立成一篇博客來寫。計(jì)劃撰寫。
代碼整潔之道-實(shí)驗(yàn)-重構(gòu)Args