- 好代碼 ,壞代碼
- (英)湯姆·朗
- 6857字
- 2022-12-01 19:13:19
1.3 代碼質(zhì)量的支柱
前面介紹的4個目標可以幫助我們聚焦要實現(xiàn)的根本目標,但它們并沒有提供日常編程的具體建議。努力找出更加具體的策略,幫助我們編寫符合這些目標的代碼,是有益的。本書將圍繞6個此類策略展開介紹。我將這6個策略稱為“代碼質(zhì)量的六大支柱”(或許有些言過其實)。我們將首先概述每個支柱,后續(xù)的章節(jié)將提供具體的示例,說明如何在日常編程中應用它們。
代碼質(zhì)量的六大支柱如下:
● 編寫易于理解(可讀)的代碼;
● 避免意外;
● 編寫難以誤用的代碼;
● 編寫模塊化的代碼;
● 編寫可重用、可推廣的代碼;
● 編寫可測試的代碼并適當測試。
1.3.1 編寫易于理解的代碼
考慮如下這段文本。我們有意地使其變得難以理解,因此,不要浪費太多時間去解讀。粗略地讀一遍,盡可能吸收其中的內(nèi)容。
取一個碗,我們現(xiàn)在稱之為A。取一個平底鍋,我們現(xiàn)在稱之為B。在B中裝滿水,置于爐盤上。在A中放入黃油和巧克力,前者100g,后者185g。這應該是70%的黑巧克力。將A放在B之上;等待A的內(nèi)容物融化,然后將A移到B之外。再取一個碗,我們現(xiàn)在稱之為C。在C中放入雞蛋、糖和香草香精,第一種原料放兩個,第二種185g,第三種半茶匙。混合C的內(nèi)容物。A的內(nèi)容物冷卻后,將其加入C中并混合。取一個碗,我們稱之為D。在D中放入面粉、可可粉和鹽,第一種原料50g,第二種原料35g,第三種半茶匙。完全混合D的內(nèi)容物,然后過濾到C中。充分攪拌D的內(nèi)容物使其完全混合。我們要用這種方法制作巧克力糕餅,我是不是忘記說這個了?在D中加入70g巧克力屑,充分攪拌D的內(nèi)容物。取一個烘焙模具,我們稱之為E。在E中涂上油脂并鋪上烘焙紙。將D的內(nèi)容物放入E中。我們將把你的烤爐稱為F。順便說一句,你應該將F預熱到160℃。將E放入F中20min,然后取出。讓E冷卻幾小時。
現(xiàn)在,我們提出一些問題。
● 這段文本說的是什么?
● 按照這些指示,我們最終能得到什么?
● 我們需要哪些配料?各種配料的分量是多少?
我們可以在這段文本中找到上述問題的答案,但并不容易。這段文本的可讀性很差。造成這一結(jié)果的問題很多,包括如下。
● 沒有標題,因此我們不得不通讀整段文本,以領(lǐng)會它的意義。
● 這段文本沒有很好地組成為一系列步驟(或者子問題),而是像一堵長長的文本墻。
● 用毫無益處的模糊名稱指代事物,如“A”,而不是“裝有融化后奶油和巧克力的碗”。
● 信息與需要它們的地方相隔甚遠:成分與數(shù)量相互分離,烤爐需要預熱這樣的重要指示到最后才提及。
(你可能已經(jīng)感到厭倦,沒有讀完這段文本,它是巧克力糕餅的食譜。如果你真想制作這種食物,附錄A中有一個更易于理解的版本。)
閱讀一段質(zhì)量低劣的代碼并試圖領(lǐng)會其含義,與我們剛剛閱讀巧克力糕餅食譜的體驗沒有什么不同。特別是,我們可能很難理解關(guān)于代碼的如下情況:
● 做什么;
● 怎么做;
● 需要什么成分(輸入或狀態(tài));
● 運行代碼后得到什么。
在某個時間,其他工程師很有可能必須閱讀并理解我們的代碼。如果我們的代碼在提交之前必須經(jīng)過代碼評審,那么這種情況幾乎是立刻發(fā)生的。但即便忽略代碼評審,在某個時間,其他人也會查看我們的代碼,并試圖領(lǐng)會它的作用。這可能發(fā)生在需求變化或者代碼需要調(diào)試的時候。
如果我們的代碼可讀性很差,其他工程師就不得不花費很多時間來解讀它。他們很有可能錯誤地理解它的作用,或者遺漏一些重要的細節(jié)。如果發(fā)生這種情況,代碼評審期間就不太可能發(fā)現(xiàn)缺陷,而在其他人修改我們的代碼、添加新功能時,就有可能引入新缺陷。軟件的功能都是基于代碼來完成的。如果工程師無法理解代碼的作用,也就幾乎不可能確定軟件能否正常工作。正如食譜一樣,代碼必須易于理解。
在第2章中,我們將了解到,如何通過定義正確的抽象層次來幫助實現(xiàn)可讀性。而在第5章中,我們將介紹使代碼更易理解的一些具體技術(shù)。
1.3.2 避免意外
過生日時得到一件禮物,或者彩票中獎,都是意外好事(驚喜)的例子。但是,當我們試圖完成一件特定任務時,意外通常不是好事。
想象一下,你餓了,因此決定下單購買一些比薩。你拿出電話,找到比薩店的電話號碼,撥通電話。很奇怪的是,電話那端沉默了很長時間,但最終還是接通了,有個聲音詢問你:想要什么?
“請來一份大的瑪格麗塔比薩,外送?!?/p>
“好的,你的地址?”
半個小時以后,你收到外賣,打開包裝一看卻發(fā)現(xiàn)圖1-3中的情景。

圖1-3 如果你以為是在和一家比薩店通話,實際上卻是一家墨西哥餐廳,那么你的訂單仍然有意義,但送來的可能是意想不到的東西
哇,這真是意外。顯然,有人將“margherita”(一種比薩的名稱)誤聽成“margarita”(一種雞尾酒的名稱)。但這是件怪事,因為這家比薩店沒有提供雞尾酒。
原來,你手機上使用的定制撥號應用添加了一個新的“智能”功能。應用開發(fā)者發(fā)現(xiàn),當用戶撥打一家餐廳的電話卻遇到忙線的情況時,80%的人會立刻致電另一家餐廳,因此,他們創(chuàng)建了一個節(jié)約時間的方便功能:當你撥打一個應用識別為餐廳的電話號碼且遇到忙線,該應用將無縫地撥打電話簿中下一家餐廳的電話號碼。
在這個例子中,下一家餐廳恰好是你最喜歡的墨西哥餐廳,而不是你以為正在撥打的比薩店。墨西哥餐廳肯定提供瑪格麗塔雞尾酒,而不是比薩。應用開發(fā)者的意圖很好,也認為這種功能可以方便用戶的生活,但他們創(chuàng)建了一個會帶來某種意外的系統(tǒng)。我們依賴自己對電話的心理模型,根據(jù)聽到的聲音確定發(fā)生的事情。重要的是,如果我們聽到語言應答,心理模型就會告訴我們已經(jīng)接通剛剛撥打的電話號碼。
定制撥號應用的這個新功能改變了應用的表現(xiàn),超出我們的預期。它打破了我們的心理模型假設,即語音應答意味著我們已經(jīng)接通剛剛撥打的電話號碼。這個功能或許很有用,但因為其行為超出普通人的心理模型,就必須明確告知人們發(fā)生的情況,例如用語音信息告訴人們撥打的電話號碼正忙,詢問是否愿意撥打另一家餐廳的電話號碼。
可以將這個定制撥號應用類比為一段代碼。其他工程師在使用我們的代碼時將名稱、數(shù)據(jù)類型和常見約定作為線索,以構(gòu)建一個心理模型,用于預測我們的代碼以什么為輸入、完成什么功能、返回什么結(jié)果。如果我們的代碼行為超出這個心理模型,就很有可能導致軟件潛藏缺陷。
在致電比薩店的例子中,即便在發(fā)生意外情況之后,一切似乎仍正常運轉(zhuǎn):你點了一個瑪格麗塔比薩,餐廳也樂于效勞。直到很久以后,錯誤已無法糾正,你才發(fā)現(xiàn)無意間點了一杯雞尾酒而不是一份比薩。這與軟件系統(tǒng)中的代碼完成意外工作時常常發(fā)生的情況很類似:因為代碼調(diào)用者沒有預料到這種意外,這些代碼將一無所知地繼續(xù)執(zhí)行。在一段時間里,一切看起來都很正常,但隨后出現(xiàn)可怕的錯誤,程序處于無效狀態(tài),或者將一個奇怪的值返回給用戶。
即便有著最好的意圖,編寫提供一些有用或者“聰明”功能的代碼仍有造成意外的風險。如果代碼做了某些意外的事情,使用代碼的工程師不會知道也不會思考處理那種情況的方法。這往往會導致系統(tǒng)“跛行”,直到在遠離問題代碼的地方出現(xiàn)明顯的古怪表現(xiàn)。或許,這只會產(chǎn)生一個有些惱人的缺陷,但也可能造成破壞重要數(shù)據(jù)的災難性問題。我們應該提防代碼中的意外情況,并盡可能避免。
在第3章中,我們將看到,代碼契約是一種有助于解決這個問題的基礎技術(shù)。第4章在介紹軟件錯誤時提到,如果不能正確提示或處理這些錯誤,就可能導致意外情況。第6章將關(guān)注避免意外的一些更具體的技術(shù)。
1.3.3 編寫難以誤用的代碼
在電視機的背部,我們可能會看到如圖1-4所示的接口。我們可以在這些接口里插入不同的線纜。重要的是,電視機廠商通過將不同的接口設計成不同的形狀,可以防止用戶將電源線插進HDMI接口中。

圖1-4 電視機廠商有意將電視機背部的接口做成不同形狀,以避免用戶插入錯誤的線纜
想象一下,如果電視機廠商沒有這么做,而是將每個接口做成相同的形狀,將會出現(xiàn)什么情況。你認為會有多少人在電視機背部摸索的時候不小心將線纜插進錯誤的接口里?如果將HDMI線纜插到電源接口里,電視機可能無法工作,雖然讓人煩惱,但也不算太可怕。但如果有人將電源線插進HDMI接口里,就會燒毀電視機的電路板。
我們所寫的代碼常常被其他代碼調(diào)用,這有點像是一臺電視機的背部。我們預計其他代碼會“插入”某種東西,比如輸入?yún)?shù),或者在調(diào)用前將系統(tǒng)置于某個狀態(tài)。如果將錯誤的東西“插入”代碼,就可能造成某些破壞:系統(tǒng)崩潰、數(shù)據(jù)庫永久性損壞或者丟失某些重要數(shù)據(jù)。即便沒有造成破壞,代碼也很有可能無法正常工作。我們的代碼被調(diào)用是有原因的,插入不正確的內(nèi)容,可能意味著一項重要的任務沒有執(zhí)行,或者某些古怪的行為沒有引起注意。
通過編寫很難或不可能被誤用的代碼,我們可以最大限度地提高代碼持續(xù)正常工作的概率。針對這個問題,有許多實用的解決方法。第3章介紹的代碼契約(類似于避免意外)是有助于編寫難以誤用的代碼的基礎技術(shù)。第7章將介紹編寫難以誤用的代碼的一些更為具體的技術(shù)。
1.3.4 編寫模塊化的代碼
模塊化意味著一個對象或系統(tǒng)由可獨立替換的更小的組件組成。為了說明這個概念以及模塊化的好處,我們考慮圖1-5中的兩個玩具。

圖1-5 模塊化的玩具很容易重新配置,而縫合起來的玩具則極難重新配置
圖1-5左側(cè)的玩具是高度模塊化的。頭部、手臂、手掌和腿都很容易獨立替換,而不會影響到玩具的其他部分。相反,圖1-5右側(cè)的玩具是非模塊化的。沒有輕松的方法可以替換頭部、手臂、手掌或腿。
模塊化系統(tǒng)(如圖1-5左側(cè)的玩具)的特征之一是,不同組件有明確定義的接口,相互作用的點盡可能少。如果我們將一只手掌當成組件,那么左側(cè)的玩具只有一個交互點和一個簡單的接口:一根釘子,以及一個與之適配的小孔。而右側(cè)的玩具在手掌和身體其他部位之間有一個極其復雜的接口:手掌和手臂上有20多圈相互交織的線。
現(xiàn)在想象一下,如果我們的任務是維護這些玩具,某天經(jīng)理告訴我們一個新需求:手掌上要有手指。我們更愿意應對哪一個玩具/系統(tǒng)?
對于左側(cè)的玩具,我們可以制造一只新設計的手掌,輕松地替換現(xiàn)有的手掌。如果兩周以后,經(jīng)理改變了主意,我們可以恢復玩具原來的配置,而不會產(chǎn)生任何麻煩。
至于右側(cè)的玩具,我們可能不得不拿出剪刀,剪掉那20多圈線,然后直接將新的手掌縫到玩具上。在這個過程中,我們可能會損壞這個玩具,如果兩周后經(jīng)理改變主意,我們就要同樣費盡力氣將玩具恢復成原有配置。
軟件系統(tǒng)和代碼庫與這些玩具非常相似。將代碼分解為獨立模塊,其中兩個相鄰模塊的交互發(fā)生在單一位置、使用明確定義的接口,往往是很有好處的。這有助于確保代碼更容易適應變化的需求,因為一項功能的變化不需要對所有地方進行大量修改。
模塊化系統(tǒng)通常也更容易理解和推演。因為系統(tǒng)被分解為容易控制的小功能塊,各功能塊之間的交互有明確的定義和文檔。這增加了代碼一開始就能正常工作,并在未來持續(xù)工作的可能性——因為工程師更不容易誤解代碼的作用。
在第2章中,我們將了解如何創(chuàng)建清晰的抽象層次,這是引導我們編寫出更具模塊化特性的代碼的一種基礎技術(shù)。在第8章中,我們還將了解一系列使代碼更加模塊化的具體技術(shù)。
1.3.5 編寫可重用、可推廣的代碼
可重用性和可推廣性這兩個概念很類似,但略有不同。
● 可重用性的含義是某個系統(tǒng)可在多種場景下用于解決同一個問題。手鉆是一種可重用工具,因為它可以在墻、地板和天花板上鉆孔。問題是相同的(需要鉆一個孔),但場景不同(鉆墻、鉆地板和鉆天花板)。
● 可推廣性的含義是某個系統(tǒng)可用于解決多個概念相近但有細微差異的問題。手鉆也是具有可推廣性的工具,因為它可以用于鉆孔,也可以將螺釘固定到某個物體上。制造商認識到,旋轉(zhuǎn)是適用于轉(zhuǎn)孔和固定螺釘?shù)耐ㄓ脝栴},因此它們造出可通用于這兩個問題的工具。
在手鉆的例子中,我們能立刻認識到這兩個特性的好處。想象一下,如果我們需要4種不同的工具。
● 只能在平舉狀態(tài)工作的鉆孔機——只能用于鉆墻。
● 只能垂直向下工作的鉆孔機——只能用于鉆地板。
● 只能垂直向上工作的鉆孔機——只能用于鉆天花板。
● 用來固定螺釘?shù)碾妱勇萁z刀。
我們必須花很多錢購買這一套4種工具,將更多的東西帶在身上,給4組電池充電——這都是浪費。幸虧有人發(fā)明了既可重用又可推廣的手鉆,我們只需要一種工具就能完成上述所有工作。不用猜也知道,手鉆在這里又是對代碼的一種類比。
創(chuàng)建代碼需要花費時間和精力,一旦創(chuàng)建完畢,還需要持續(xù)投入時間和精力進行維護。創(chuàng)建代碼也并非沒有風險:盡管我們小心翼翼,編寫的一些代碼仍會包含缺陷,寫得越多,出現(xiàn)缺陷的可能性越大。重點在于,我們在代碼庫中留下的代碼行數(shù)越少越好。這聽起來可能有些奇怪,我們不是通過寫代碼得到報酬的嗎?但實際上,我們得到工資,是因為能夠解決某個問題,代碼只是一種手段。如果我們可以解決問題,同時花費更少的精力,降低我們不小心引入缺陷而導致其他問題的概率,就太好了。
編寫可重用、可推廣的代碼,我們(和其他人)就可以在代碼庫的多個地方和場景中使用它們,解決不止一個問題。這能節(jié)約時間和精力,并使我們的代碼更加可靠,因為我們往往重用已在外部經(jīng)過考驗的邏輯,其中的缺陷可能已經(jīng)被發(fā)現(xiàn)和修復。
更具模塊化特性的代碼往往也有更好的可重用性和可推廣性。與模塊化相關(guān)的章節(jié)與可重用性和可推廣性的主題關(guān)系緊密。此外,第9章將介紹一些提高代碼可重用性、可推廣性的專用技術(shù)和考慮因素。
1.3.6 編寫可測試的代碼并適當測試
正如我們在前面的軟件開發(fā)與部署過程(見圖1-2)中所見到的,在確保最終不會將有缺陷和不完善的功能投入運行的過程中,測試是至關(guān)重要的一環(huán)。它們往往是這一過程中兩個關(guān)鍵點的主要保障(見圖1-6)。
● 防止有缺陷或者不完善的功能提交到代碼庫。
● 確保阻止有缺陷或不完善的功能發(fā)行并投入運行。
因此,測試對確保代碼可用并持續(xù)正常工作是必不可少的。

圖1-6 為了最大限度地防止有缺陷和不完善的功能進入代碼庫,并確保它們不會對外發(fā)行,測試至關(guān)重要
在軟件開發(fā)中,測試的重要性如何強調(diào)都不為過。你以前肯定多次聽到這一說法,很容易將其視為老生常談,但它確實重要。正如我們在本書的很多地方看到的那樣。
● 軟件系統(tǒng)和代碼庫往往太過龐大和復雜,一個人不可能了解所有細節(jié)。
● 人(即便是智力超群的工程師)都會犯錯。
這或多或少都是生活中的事實。除非我們用測試來鎖定代碼的功能,否則這些功能就會習慣性地與我們(以及我們的代碼)糾纏在一起。
代碼質(zhì)量的這一支柱包含兩個重要的概念:“編寫可測試的代碼”以及“適當測試”。測試和可測試性相關(guān),但考慮的因素不同。
● 測試——顧名思義,這與測試我們的代碼或者軟件有關(guān)。測試可能是人工進行,也可能是自動進行。作為工程師,我們通常努力編寫測試代碼來執(zhí)行“真實”代碼,并檢查一切表現(xiàn)是否如同預期。測試有不同級別。你可能使用的3種最常見的測試級別如下。(請注意,這并不是完整的列表。測試有許多分類方法,不同組織往往使用不同的術(shù)語。)
? 單元測試——這種測試通常測試代碼的小單元(如單個函數(shù)或類)。單元測試是測試工程師在日常編程中最經(jīng)常使用的測試級別,也是本書唯一詳細介紹的測試級別。
? 集成測試——系統(tǒng)通常由多個組件、模塊或子系統(tǒng)組成。將這些組件和子系統(tǒng)連接到一起的過程稱為集成。集成測試試圖確保這些集成正常工作,而且一直保持正常。
? 端到端(E2E)測試——測試整個軟件系統(tǒng)從頭至尾的典型流程。如果待測軟件是一個在線購物系統(tǒng),E2E測試的一個例子是自動驅(qū)動瀏覽器,確保用戶能夠完成一次購物流程。
● 可測試性——這指的是“真實代碼”(相對于測試代碼),并描述該代碼在測試中的表現(xiàn)。某個事物“可測試”的概念在子系統(tǒng)或系統(tǒng)級別上也適用??蓽y試性往往與模塊化高度關(guān)聯(lián),模塊化程度越高的代碼(或系統(tǒng))越容易測試。想象一下,某汽車制造商正在開發(fā)一種緊急行人防撞制動系統(tǒng)。如果該系統(tǒng)的模塊化程度不高,測試它的唯一方式可能是將其安裝在真實的汽車上,將車開到一個真人面前,檢查車輛是否會自動停下。如果情況果真如此,那么該系統(tǒng)所能測試的場景有限,因為每次測試的成本非常高:制造一輛整車,租用一條測試道路,并讓一個真人冒險扮演路上的行人。如果這種緊急制動系統(tǒng)是一個單獨的模塊,可在真實車輛之外運行,可測試性就更高了?,F(xiàn)在測試可以通過如下方式進行:向該系統(tǒng)提供預先錄制的行人走出的視頻,檢查它是否為緊急制動系統(tǒng)輸出正確的信號。這樣的測試非常簡易、經(jīng)濟且安全,可以對成千上萬種不同的行人狀況進行測試。
如果代碼不可測試,也就不可能對其進行“適當”測試了。為了確保我們編寫的代碼是可測試的,最好在編寫代碼時不斷地問自己一個問題:“我們將如何測試這些代碼?”因此,測試不應該是“馬后炮”,而應該是編寫代碼各個階段不可分割的基本組成部分。第10章和第11章介紹的都是關(guān)于測試的內(nèi)容,但因為測試對編寫代碼必不可少,所以我們在本書的許多地方都會提到。
注意:測試驅(qū)動開發(fā)
因為測試是代碼編寫工作中必不可少的部分,一些工程師倡導在編寫代碼之前先編寫測試的做法。這是測試驅(qū)動開發(fā)(Test-Driven Development,TDD)過程所支持的做法之一。我們將在10.5節(jié)中進一步討論這個問題。
軟件測試是一個很廣泛的主題,坦率地說,本書無法做到面面俱到。在本書中,我們將介紹代碼單元測試中重要且常被忽視的特征,因為它們在日常編程過程中通常非常有用。但請注意,直到本書的最后,我們對軟件測試的介紹也只是皮毛。
- Netty權(quán)威指南
- DevSecOps企業(yè)級實踐:理念、技術(shù)與案例
- 產(chǎn)品經(jīng)理入門攻略
- Cadence系統(tǒng)級封裝設計:Allegro SiP/APD設計指南
- 鑄魂:軟件定義制造
- Spring Cloud Alibaba大型微服務架構(gòu)項目實戰(zhàn)(上冊)
- Python跨平臺應用軟件開發(fā)實戰(zhàn)
- Unity手機游戲開發(fā):從搭建到發(fā)布上線全流程實戰(zhàn)
- 中國軟件工程師:工作、生活與觀念
- 全棧Monorepo開發(fā)實戰(zhàn)(Vue 3+Fastify+Deno+pnpm)
- 移動終端應用軟件開發(fā)實戰(zhàn)
- 統(tǒng)信UOS應用開發(fā)詳解
- 現(xiàn)代交換技術(shù)(第3版)
- 構(gòu)建跨平臺APP:jQuery Mobile移動應用實戰(zhàn)(第2版) (跨平臺移動開發(fā)叢書)
- 大話軟件工程:需求分析與軟件設計