源代碼有兩種不同的用戶:程序員和計(jì)算機(jī)局雄。一方面甥啄,計(jì)算機(jī)既能處理干凈、結(jié)構(gòu)良好的代碼哎榴,也能處理混亂的代碼型豁。另一方面,程序員對代碼的可讀性很敏感尚蝌。甚至是代碼中的空白迎变、正確使用縮進(jìn)(這與計(jì)算機(jī)完全無關(guān))也決定了代碼容易理解或難以理解。
此外,代碼的可讀性也提高了可靠性飘言,因?yàn)橥ǔ2蝗菀纂[藏一些bug衣形。并且提高了可維護(hù)性,因?yàn)樗菀仔薷摹?/p>
關(guān)于可讀性的一些想法
編寫可讀的代碼是一門被低估的技術(shù)姿鸿,學(xué)校很少教授這種技術(shù)谆吴,但它卻與軟件的可靠性、維護(hù)和發(fā)展密切相關(guān)苛预。程序員通常學(xué)習(xí)用機(jī)器容易理解的東西來實(shí)現(xiàn)所需功能的代碼句狼。這個(gè)編碼過程需要添加一層又一層的抽象來將功能分解為更小的單元。
Java語言中热某,這些抽象是包腻菇、類和方法。如果整個(gè)系統(tǒng)足夠大昔馋,就沒有程序員可以單獨(dú)控制整個(gè)代碼庫筹吐。有些開發(fā)者對某個(gè)特定的業(yè)務(wù)有一個(gè)縱深的認(rèn)識。其他開發(fā)人員可能只負(fù)責(zé)一個(gè)抽象層并維護(hù)它的API秘遏。他們都需要經(jīng)常閱讀和理解別人編寫的代碼丘薛。提高可讀性意味著將程序員理解一段代碼所需的時(shí)間最小化。
如何編寫可讀的程序?用一句關(guān)于表現(xiàn)力的格言來總結(jié)就是邦危,簡單而直接地說出你的意思洋侨。事實(shí)上,可讀性意味著清楚地表達(dá)代碼意圖倦蚪。統(tǒng)一建模語言(UML)設(shè)計(jì)師之一格雷迪·布克(Grady Booch)給出了一個(gè)自然的類比:干凈的代碼讀起來就像優(yōu)美的散文凰兑。
寫好散文不是簡單地遵循一套固定的規(guī)則,而是需要練習(xí)和閱讀著名作家的偉大文章审丘,這一過程可能需要數(shù)年時(shí)間吏够。幸運(yùn)的是,與自然語言相比,計(jì)算機(jī)代碼的表達(dá)能力非常有限锅知,所以編寫出干凈的代碼比寫優(yōu)美的散文更容易播急,或者至少更有條理。業(yè)界對重構(gòu)和編寫干凈的代碼越來越感興趣售睹∽可讀性已經(jīng)成為敏捷開發(fā)中最重要的關(guān)注點(diǎn)之一。
試圖使用一組簡單的數(shù)字指標(biāo)(如標(biāo)記)來評估可讀性昌妹。標(biāo)識符的長度捶枢、表達(dá)式中出現(xiàn)的括號的數(shù)量,等等飞崖。這項(xiàng)工作仍在進(jìn)行中烂叔,要達(dá)成一個(gè)穩(wěn)定的共識還有很長的路要走,我們接下來將通過一個(gè)例子來說明和解釋代碼可讀性的一些改進(jìn)點(diǎn)固歪。
整理connectTo方法
現(xiàn)在我們將注意力轉(zhuǎn)向一個(gè)名叫connectTo的方法蒜鸡,該方法會將兩個(gè)組里面的容器進(jìn)行合并,并且容器里面的水會被均分牢裳。對其進(jìn)行重構(gòu)以提高可讀性逢防。首先查看初始版本的實(shí)現(xiàn):
public void connectTo(Container other) {
// 如果兩個(gè)容器已經(jīng)連接,則不做任何事情
if (group==other.group) return;
int size1 = group.size(),
size2 = other.group.size();
double tot1 = amount * size1,
tot2 = other.amount * size2,
newAmount = (tot1 + tot2) / (size1 + size2);
// 合并兩個(gè)組
group.addAll(other.group);
// 更新要連接的所有容器的組
for (Container c: other.group) { c.group = group; }
// 更新所有新連接的容器
for (Container c: group) { c.amount = newAmount; }
}
這里有一個(gè)缺陷:它包含了大量的注釋蒲讯,試圖解釋每一行代碼的含義忘朝。有些程序員關(guān)心他們的同事,想要他們更好的理解代碼判帮,自然會添加這樣的注釋局嘁。然而,這并不是實(shí)現(xiàn)容易理解這一目標(biāo)的最有效的方法脊另。更好的選擇是使用提取方法的方式來進(jìn)行重構(gòu)导狡。
可讀性提示:“提取方法”重構(gòu)規(guī)則——提取可以實(shí)現(xiàn)某一個(gè)小功能的代碼塊轉(zhuǎn)到一個(gè)新方法并使用描述性名稱约巷。
我們可以在connectTo方法中應(yīng)用這種技術(shù)偎痛。事實(shí)上,我們可以拆分5個(gè)新的方法独郎,以及獲得一個(gè)新的踩麦、可讀性更強(qiáng)的connectTo方法:
/** Connects this container with another.
*
* @param other The container that will be connected to this one
*/
public void connectTo(Container other) {
if (this.isConnectedTo(other)) return;
double newAmount = (groupAmount() + other.groupAmount()) /
(groupSize() + other.groupSize());
mergeGroupWith(other.group);
setAllAmountsTo(newAmount);
}
這個(gè)方法更短,可讀性更強(qiáng)氓癌。如果你試著把這個(gè)方法大聲讀出來谓谦,你會發(fā)現(xiàn)它幾乎可以變成可以被理解的一個(gè)短文。為此贪婉,我們引入了五種適當(dāng)?shù)闹С址椒ǚ粗唷J聦?shí)上,很多業(yè)內(nèi)的大佬都認(rèn)為長方法是一種不好的代碼味道,提取方法來消除這種壞味道是普遍被采納的一種重構(gòu)技術(shù)。
添加注釋只能解釋部分代碼才顿,而提取方法既解釋代碼又隱藏生成過程代碼——將代碼提取到單個(gè)方法中莫湘。在這個(gè)例子中,它會使原來的方法抽象級別保持在更高郑气、更統(tǒng)一的高度幅垮,避免了舊版本代碼中的高層API解釋和底層實(shí)現(xiàn)錯綜復(fù)雜地交織在一起。
用查詢替換局部變量是另一種可用于connectTo方法的重構(gòu)技術(shù)尾组。
可讀性提示:“用查詢替換局部變量”重構(gòu)規(guī)則——更改局部變量忙芒,通過調(diào)用一個(gè)計(jì)算其值的新方法來替換該量。你可以將此技術(shù)應(yīng)用于局部變量newAmount讳侨,該變量只分配一次呵萨,然后用作setAllAmountsTo方法的參數(shù)。應(yīng)用該技術(shù)可以直接刪除變量newAmount爷耀,并將connectTo方法的最后兩行替換為以下內(nèi)容甘桑。
mergeGroupWith(other.group);
setAllAmountsTo(amountAfterMerge(other));
amountAfterMerge是一個(gè)計(jì)算合并后的每個(gè)容器水量的新方法。但是歹叮,稍加思考就會發(fā)現(xiàn)跑杭,amountAfterMerge方法需要克服很多困難才能完成任務(wù),因?yàn)樵谡{(diào)用方法時(shí)咆耿,兩個(gè)group已經(jīng)完成了合并德谅。group已經(jīng)包含了other的group。一個(gè)很好的折衷方案是將計(jì)算新水量的表達(dá)式封裝到一個(gè)新方法中萨螺,同時(shí)保留局部變量窄做,以便在合并組之前計(jì)算出新的量。
final double newAmount = amountAfterMerge(other);
mergeGroupWith(other.group);
setAllAmountsTo(newAmount);
總而言之慰技,我不建議進(jìn)行這種重構(gòu)椭盏,如抽出5個(gè)方法版本中的代碼所示newAmount表達(dá)式是可讀的,不需要隱藏在單獨(dú)的方法中吻商。當(dāng)它替換的表達(dá)式很復(fù)雜或在類中多次出現(xiàn)時(shí)掏颊,“用查詢替換局部變量”規(guī)則通常更有用。
現(xiàn)在看看可讀版本中connectTo方法的五個(gè)新支持方法艾帐。在這五個(gè)方法中乌叶,有兩個(gè)最好聲明為私有的,因?yàn)樗鼈兛赡軐?dǎo)致容器對象處于不一致的狀態(tài)柒爸,不應(yīng)該從類外部調(diào)用准浴。他們是mergeGroupWith方法和setAllAmountsTo方法。
mergeGroupWith方法合并兩組容器而不更新它們的水量捎稚。如果有人單獨(dú)從外部調(diào)用它乐横,很可能使一些或所有容器的水量發(fā)生錯誤求橄。這個(gè)方法只有在使用它的上下文中才有意義:在connectTo方法的末尾,然后調(diào)用setAllAmountsTo方法葡公。事實(shí)上谈撒,它是否真的應(yīng)該獨(dú)立成一個(gè)方法是有爭議的。
一方面匾南,讓它獨(dú)立可以通過給予它一個(gè)好名字來解釋它的用途啃匿,而不是像開始的版本那樣使用注釋解釋。另一方面蛆楞,獨(dú)立出來的方法可能在錯誤的上下文中被調(diào)用溯乒。因?yàn)槲覀兪菫榱丝勺x性而優(yōu)化的,所以創(chuàng)建獨(dú)立的方法會更好一點(diǎn)豹爹。類似的權(quán)衡setAllAmountsTo方法也適用裆悄。
private void mergeGroupWith(Set<Container> otherGroup) {
group.addAll(otherGroup);
for (Container x: otherGroup) {
x.group = group;
}
}
private void setAllAmountsTo(double amount) {
for (Container x: group) {
x.amount = amount;
}
}
私有方法不值得用Javadoc注釋。它們只在類內(nèi)部使用臂聋,所以很少有人覺得有必要了解他們的細(xì)節(jié)光稼。因此,添加注釋不是太有必要的孩等。注釋的成本并不限于編寫它們所需的時(shí)間艾君。就像其他源代碼一樣,它需要維護(hù)肄方,否則可能會過時(shí)冰垄。也就是說,隨著版本的迭代权她,注釋和它所描述的代碼不同步了虹茶。
記住:過時(shí)的評論比沒有評論更糟糕! 用描述性名稱代替注釋并不能避免這種風(fēng)險(xiǎn)。如果編寫的代碼功能和名稱不符了隅要,然后最終仍然可能產(chǎn)生一些過時(shí)的名稱蝴罪,這和過時(shí)的注釋同樣糟糕。
其他三種新的支持方法都是只讀特性步清,不會帶來任何不良影響要门。我們不應(yīng)該輕易做出讓他們公有化的決定。添加到類中任何公共成員的后續(xù)維護(hù)成本都要比添加相同的私有成員的成本大得多尼啡。公共方法的額外成本包括:
- 描述其功能的適當(dāng)注釋;
- 條件檢查暂衡,以處理可能不正確的輸入內(nèi)容;
- 一套完整的測試询微,以確保其正確性崖瞭。
connectTo方法的三個(gè)新的公有支持方法:
/** Checks whether this container is connected to another one.
*
* @param other the container whose connection with this will be
checked
* @return <code>true</code> if this container is connected
* to <code>other</code>
*/
public boolean isConnectedTo(Container other) {
return group == other.group;
}
/** Returns the number of containers in the group of this
container.
*
* @return the size of the group
*/
public int groupSize() {
return group.size();
}
/** Returns the total amount of water in the group of this
container.
*
* @return the amount of water in the group
*/
public double groupAmount() {
return amount * group.size();
}
順便說一下,isConnectedTo方法還改進(jìn)了類的可測試性撑毛,因?yàn)樗挂郧霸趯?shí)現(xiàn)中需要推測的內(nèi)容都變成了直接可測試的书聚。實(shí)現(xiàn)connectTo的六個(gè)方法都非常短唧领,其中connectTo是最長的方法本身只有6行。簡潔是干凈代碼的主要原則之一雌续。