以addWater方法為例講解代碼可讀性的改進(jìn)方案
/** Adds water to this container.
* A negative <code>amount</code> indicates removal of water.
* In that case, there should be enough water in the group
* to satisfy the request.
*
* @param amount the amount of water to be added
*/
public void addWater(double amount) {
double amountPerContainer = amount / group.size();
for (Container c: group) {
c.amount += amountPerContainer;
}
}
前置條件:如果參數(shù)是負(fù)值断箫,則組中要有足夠的水量。
后置條件:將添加的水平均分配給該組中的所有容器秋冰。
懲罰:拋出IllegalArgumentException仲义。
注意,代碼清單中的注釋并沒(méi)有提到如果輸入內(nèi)容違反了前置條件會(huì)有怎樣的反應(yīng)剑勾,即需要移除的水量大于實(shí)際存在的水量埃撵。這是因?yàn)樵搶?shí)現(xiàn)并沒(méi)有檢查前置條件,而是允許容器中的水量為負(fù)值甥材。Javadoc風(fēng)格規(guī)范和 Effective Java一書(shū)都建議使用@throws或@exception標(biāo)簽(這兩個(gè)標(biāo)簽是等價(jià)的)來(lái)說(shuō)明方法可能拋出的非檢查型異常盯另。將下面這行代碼加到方法注釋里面就可以了。
@throws IllegalArgumentException
if an attempt is made to remove more water than actually
present
快速瀏覽一下官方的Java API文檔洲赵,就會(huì)發(fā)現(xiàn)這確實(shí)是標(biāo)準(zhǔn)做法鸳惯。舉 個(gè)例子商蕴,ArrayList類的get(int index)方法返回列表中給定 index位置的元素,該方法的文檔注釋說(shuō)明了如果索引越界芝发,則會(huì)拋出非檢查型異常IndexOutOfBoundsException绪商。
可讀性的終極思考
你可以很容易地把本章中的建議應(yīng)用到大多數(shù)甚至所有真實(shí)場(chǎng)景中「ňǎ可讀性可能會(huì)與其他質(zhì)量目標(biāo)(如時(shí)間效率或空間效率)有沖突格郁,但在大多數(shù)場(chǎng)景 中,可讀性應(yīng)該占上風(fēng)独悴。當(dāng)一款軟件由于要修復(fù)bug或?qū)崿F(xiàn)新功能而需要不斷演進(jìn)時(shí)例书,保持良好的可讀性會(huì)讓我們受益匪淺。 但是刻炒,不應(yīng)該把代碼的整潔性和算法的簡(jiǎn)潔性混為一談决采。
我們并不提倡為了追求可讀性而拋棄高效的算法,反而選擇低效的算法坟奥。恰恰相反树瞭,應(yīng)該選擇最適合的算法來(lái)完成任務(wù),然后努力編寫(xiě)最整潔的代碼爱谁。整潔的代碼當(dāng)然優(yōu)于為追求性能而采取的“奇技淫巧”晒喷,但軟件工程的合理做法才是更高的追求。 只在少數(shù)場(chǎng)景下访敌,可讀性要么是一種奢侈凉敲,要么是需要刻意避免的。 典型例子是時(shí)間緊張的編程挑戰(zhàn)賽捐顷,比如編程馬拉松或編程大賽荡陷。此時(shí),參賽者需要以最快的速度寫(xiě)出代碼迅涮。代碼只要能工作就行废赞,寫(xiě)完即可扔掉。任何延遲都是一種開(kāi)銷叮姑,也就不必考慮編碼風(fēng)格了唉地。
另一個(gè)例子是,一些公司不希望其他任何人能分析其源代碼传透,包括軟件的合法用戶耘沼。他們希望通過(guò)隱藏或混淆源代碼來(lái)隱藏其算法或數(shù)據(jù)。此時(shí)朱盐,他們會(huì)很自然地放棄代碼的可讀性群嗤,刻意編寫(xiě)非常晦澀難懂的代碼來(lái)完成工作兵琳。事實(shí)上狂秘,有一種名為混淆器(obfuscator)的特殊軟件骇径,用途正是將一個(gè)程序翻譯成另一個(gè)功能相同但人類極難理解的程序。
你可以混淆用任何編程語(yǔ)言開(kāi)發(fā)的程序 者春,從機(jī)器代碼到Java字節(jié)碼或源代碼破衔。在網(wǎng)上搜索“Java混淆器”就可以找到大量的開(kāi)源和商業(yè)混淆工具。因?yàn)檫@類工具的存在钱烟,即使最敏感的公司也可以在開(kāi)發(fā)過(guò)程中保持代碼是整潔晰筛、可讀的(有利于提高軟件質(zhì)量), 然后在公開(kāi)發(fā)布之前使用混淆工具把它們變得晦澀難懂拴袭。
來(lái)點(diǎn)兒新鮮的
把編寫(xiě)可讀代碼的原則應(yīng)用到一個(gè)不同的例子中读第。這是一個(gè)單獨(dú)的方法,它接收一個(gè)double類型的二維數(shù)組稻扬,然后對(duì)其做一些事情卦方。我們故意把該方法的代碼寫(xiě)得很隨意羊瘩,雖然算不上晦澀泰佳,但也不太可讀。作為一個(gè)練習(xí)尘吗,在繼續(xù)閱讀之前逝她,請(qǐng)?jiān)囍斫馑隽耸裁础?/p>
public static void f(double[][] a) {
int i = 0, j = 0;
while (i<a.length) {
if (a[i].length != a.length)
throw new IllegalArgumentException();
i++;
}
i = 0;
while (i<a.length) {
j = 0;
while (j<i) {
double temp = a[i][j];
a[i][j] = a[j][i];
a[j][i] = temp;
j++;
}
i++;
}
}
你感覺(jué)到痛苦了嗎?那些while循環(huán)和毫無(wú)意義的變量名真的讓人頭昏腦漲睬捶。想象一下黔宛,用這樣的風(fēng)格編寫(xiě)整個(gè)程序有多么可怕吧! 或許你已經(jīng)看出來(lái)了擒贸,這個(gè)謎一樣的方法會(huì)轉(zhuǎn)置(transpose)一個(gè)方陣臀晃,即交換其行與列。第一個(gè)while循環(huán)檢查傳入的矩陣是否是方形的(行數(shù)和列數(shù)相同)介劫。由于Java二維數(shù)組表示的矩陣可能是不規(guī)則的(每一行的長(zhǎng)度可能不同)徽惋,就需要檢查每一行的長(zhǎng)度是否都與行數(shù)相同。下面是添加了注釋的版本座韵,幫助你理解代碼的各個(gè)部分险绘。
public static void f(double[][] a) {
int i = 0, j = 0;
while (i<a.length) { ? 遍歷每一行
if (a[i].length != a.length) ? 如果該行的長(zhǎng)度是“錯(cuò)誤”的
throw new IllegalArgumentException();
i++;
}
i = 0;
while (i<a.length) { ? 遍歷每一行
j = 0;
while (j<i) { ? 遍歷前i列
double temp = a[i][j]; ? 交換a[i][j]和a[j][i]
a[i][j] = a[j][i];
a[j][i] = temp;
j++;
}
i++;
}
}
現(xiàn)在使用本章介紹的原則來(lái)提高此方法的可讀性。首先誉碴,開(kāi)始部分檢查矩陣是否為方陣的代碼特別適合使用“提取方法”這個(gè)重構(gòu)原則: 它是具有明確契約定義的一個(gè)連貫操作宦棺。一旦將其提取到單獨(dú)的方法中,就可能在其他地方使用它黔帕。因此我將它聲明為公有的代咸,并給它添加了完整的Javadoc注釋。 由于檢查是否為方陣的操作不會(huì)修改矩陣成黄,因此可以在其主循環(huán)中使用增強(qiáng)型for循環(huán)呐芥。
然后白华,轉(zhuǎn)置矩陣的方法會(huì)調(diào)用isSquare方法,并使用兩個(gè)直白的for 循環(huán)來(lái)執(zhí)行轉(zhuǎn)置操作贩耐。這時(shí)候就不能使用增強(qiáng)型for循環(huán)了弧腥,因?yàn)樾枰泻土械乃饕齺?lái)完成交換工作。 同時(shí)潮太,還可以改進(jìn)變量和方法本身的命名管搪,讓其更可讀≌÷颍可以保留i和 j作為行和列的索引更鲁,因?yàn)樗鼈兪菙?shù)組索引的標(biāo)準(zhǔn)命名。
/** Transposes a square matrix
*
* @param matrix a matrix
* @throws IllegalArgumentException if the given matrix is not
square
*/
public static void transpose(double[][] matrix) {
if (!isSquare(matrix)) {
throw new IllegalArgumentException(
"Can't transpose a nonsquare matrix.");
}
for (int i=0; i<matrix.length; i++) { ? 遍歷每一行
for (int j=0; j<i; j++) { ? 遍歷前i列
double temp = matrix[i][j]; ? 交換a[i][j]和a[j][i]
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
真實(shí)世界的用例
你已經(jīng)學(xué)習(xí)并實(shí)際應(yīng)用了一些非常重要的提高代碼可讀性的原則奇钞。這里列舉幾個(gè)案例來(lái)幫助你理解提高代碼可讀性在真實(shí)世界中的重要性澡为。 想象一下,你是一家小型初創(chuàng)公司的創(chuàng)始人之一景埃,并且成功幫公司中標(biāo)媒至,要為一家天然氣基礎(chǔ)設(shè)施管理公司開(kāi)發(fā)軟件,項(xiàng)目目標(biāo)是實(shí)施監(jiān)管法律谷徙。一切看起來(lái)都很好:你拿到了一個(gè)好項(xiàng)目拒啰,而且意識(shí)到,由于法律很少改變完慧,軟件交付后谋旦,你們將能在維護(hù)合同存續(xù)期間繼續(xù)享受勞動(dòng)果實(shí)獲取收益。
你和同事們做出了一個(gè)戰(zhàn)略性的決策屈尼,要盡可能快地交付解決方案册着,給客戶留下深刻的印象。為此脾歧,你決定減少一些非必需的工作甲捏,比如提高代碼可讀性、維護(hù)文檔涨椒、進(jìn)行單元測(cè)試等摊鸡。幾年后,你的公司發(fā)展壯大蚕冬, 但原來(lái)的團(tuán)隊(duì)中有一半人離開(kāi)了公司免猾,而公司還和天然氣運(yùn)營(yíng)商續(xù)簽著合同。
然后有一天囤热,概率極小的事情發(fā)生了:法律法規(guī)發(fā)生了變化猎提,需要修改軟件以滿足新的需求。這時(shí)你才意識(shí)到理解現(xiàn)有代碼的工作原理比實(shí)現(xiàn)新需求更難旁蔼。代碼的可讀性非常重要锨苏,是軟件公司團(tuán)隊(duì)運(yùn)作的決定性因素疙教。 你是一個(gè)有熱情、有才華的開(kāi)發(fā)者伞租,渴望為開(kāi)源社區(qū)做出貢獻(xiàn)贞谓。 你有一個(gè)偉大的想法(至少你自己這么認(rèn)為):要在GitHub上分享代碼,希望它能吸引貢獻(xiàn)者葵诈,并最終被用于真正的項(xiàng)目中裸弦。你意識(shí)到可讀性是吸引貢獻(xiàn)者的關(guān)鍵因素,因?yàn)樗麄円婚_(kāi)始對(duì)你的代碼庫(kù)并不熟悉作喘,而且很可能不愿意詢問(wèn)相關(guān)的問(wèn)題理疙。
下面的例子顯示了編程界對(duì)可讀性的重視程度。
無(wú)論你使用哪種編程語(yǔ)言泞坦,都應(yīng)該盡可能地讓代碼更可讀窖贤。然而,對(duì)于某些編程語(yǔ)言而言贰锁,可讀性是語(yǔ)言層面的一種設(shè)計(jì)特性赃梧。Python是最流行的語(yǔ)言之一,可以說(shuō)原因之一就是它天然的可讀性李根。事實(shí)上槽奕,可讀性公認(rèn)十分重要,以至于語(yǔ)言設(shè)計(jì)者提出了旨在提高可讀性的著名編碼風(fēng)格規(guī)范PEP 8(Python改進(jìn)提案)房轿。
我們?cè)賮?lái)談?wù)凱ython。(是的所森,本書(shū)主要使用Java囱持,但這些原則是通用的。)Python是一種動(dòng)態(tài)類型的語(yǔ)言焕济,所以不必指定函數(shù)參數(shù)和返回值的類型纷妆。然而,PEP 484在Python 3.5版本中引入了 可選的類型提示晴弃,提供了一種聲明這些類型的標(biāo)準(zhǔn)方法掩幢。這些提示對(duì)性能完全沒(méi)有影響,也不提供運(yùn)行時(shí)類型推斷上鞠。它們的目的是提高可讀性际邻,支持更多的靜態(tài)類型檢查,從而提高了可靠性芍阎。
小結(jié)
可讀性是提高可靠性和可維護(hù)性的重要因素世曾。 可以通過(guò)結(jié)構(gòu)性優(yōu)化和外表優(yōu)化等方式提高可讀性。 提高可讀性是一個(gè)常見(jiàn)的重構(gòu)目標(biāo)谴咸。 自描述的代碼優(yōu)于實(shí)現(xiàn)注釋轮听。 應(yīng)該以標(biāo)準(zhǔn)方式編寫(xiě)詳細(xì)的骗露、格式化的文檔注釋,使其易于閱讀血巍。