提高代碼可續(xù)性的小技巧呈宇,以connectTo方法為例。

源代碼有兩種不同的用戶:程序員和計(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ù)成本都要比添加相同的私有成員的成本大得多尼啡。公共方法的額外成本包括:

  1. 描述其功能的適當(dāng)注釋;
  2. 條件檢查暂衡,以處理可能不正確的輸入內(nèi)容;
  3. 一套完整的測試询微,以確保其正確性崖瞭。

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行。簡潔是干凈代碼的主要原則之一雌续。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斩个,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子驯杜,更是在濱河造成了極大的恐慌受啥,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸽心,死亡現(xiàn)場離奇詭異滚局,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)顽频,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門藤肢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人糯景,你說我怎么就攤上這事嘁圈。” “怎么了蟀淮?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵最住,是天一觀的道長。 經(jīng)常有香客問我怠惶,道長温学,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任甚疟,我火速辦了婚禮仗岖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘览妖。我一直安慰自己轧拄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布讽膏。 她就那樣靜靜地躺著檩电,像睡著了一般。 火紅的嫁衣襯著肌膚如雪府树。 梳的紋絲不亂的頭發(fā)上俐末,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機(jī)與錄音奄侠,去河邊找鬼卓箫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛垄潮,可吹牛的內(nèi)容都是我干的烹卒。 我是一名探鬼主播闷盔,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼旅急!你這毒婦竟也來了逢勾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤藐吮,失蹤者是張志新(化名)和其女友劉穎溺拱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谣辞,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盟迟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了潦闲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攒菠。...
    茶點(diǎn)故事閱讀 38,814評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖歉闰,靈堂內(nèi)的尸體忽然破棺而出辖众,到底是詐尸還是另有隱情,我是刑警寧澤和敬,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布凹炸,位于F島的核電站,受9級特大地震影響昼弟,放射性物質(zhì)發(fā)生泄漏啤它。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一舱痘、第九天 我趴在偏房一處隱蔽的房頂上張望变骡。 院中可真熱鬧,春花似錦芭逝、人聲如沸塌碌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽台妆。三九已至,卻和暖如春胖翰,著一層夾襖步出監(jiān)牢的瞬間接剩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工萨咳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留懊缺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓某弦,卻偏偏與公主長得像桐汤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子靶壮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容