好代碼和壞代碼
要寫(xiě)出好代碼,首先需要提升品位。
很多軟件工程師寫(xiě)不好代碼,在評(píng)審他人的代碼時(shí)也看不出問(wèn)題,就是因?yàn)槿狈?duì)好代碼標(biāo)準(zhǔn)的認(rèn)識(shí)。
現(xiàn)在還有太多的軟件工程師認(rèn)為,代碼只要可以正確執(zhí)行就可以了。這是一種非常低的評(píng)價(jià)標(biāo)準(zhǔn),很多重要的方面都被忽視了。
好代碼的特性
好代碼具有以下特性。
1. 魯棒(Solid and Robust)
代碼不僅要被正確執(zhí)行,我們還要考慮對(duì)各種錯(cuò)誤情況的處理,比如各種系統(tǒng)調(diào)用和函數(shù)調(diào)用的異常情況,系統(tǒng)相關(guān)組件的異常和錯(cuò)誤。
對(duì)很多產(chǎn)品級(jí)的程序來(lái)說(shuō),異常和錯(cuò)誤處理的邏輯占了很大比例。
2. 高效(Fast)
程序的運(yùn)行應(yīng)使用盡量少的資源。資源不僅僅包括CPU,還可能包括存儲(chǔ)、I/O等。
設(shè)計(jì)高效的程序,會(huì)運(yùn)用到數(shù)據(jù)結(jié)構(gòu)和算法方面的知識(shí),同時(shí)要考慮到程序運(yùn)行時(shí)的各種約束條件。
3. 簡(jiǎn)潔(Maintainable and Simple)
代碼的邏輯要盡量簡(jiǎn)明易懂,代碼要具有很好的可維護(hù)性。對(duì)于同樣的目標(biāo),能夠使用簡(jiǎn)單清楚的方法達(dá)成,就不要使用復(fù)雜晦澀的方法。
“大道至簡(jiǎn)”,能否把復(fù)雜的問(wèn)題用簡(jiǎn)單的方式實(shí)現(xiàn)出來(lái),這是一種編程水平的體現(xiàn)。
4. 簡(jiǎn)短(Small)
在某種意義上,代碼的復(fù)雜度和維護(hù)成本是和代碼的規(guī)模直接相關(guān)的。在實(shí)現(xiàn)同樣功能的時(shí)候,要盡量將代碼寫(xiě)得簡(jiǎn)短一些。
簡(jiǎn)潔高于簡(jiǎn)短。這里要注意,某些人為了能把代碼寫(xiě)得簡(jiǎn)短,使用了一些晦澀難懂的描述方式,降低了代碼的可讀性。這種方式是不可取的。
5. 可測(cè)試(Testable)
代碼的正確性要通過(guò)測(cè)試來(lái)保證,尤其是在敏捷的場(chǎng)景下,更需要依賴可自動(dòng)回歸執(zhí)行的測(cè)試用例。
在代碼的設(shè)計(jì)中,要考慮如何使代碼可測(cè)、易測(cè)。一個(gè)比較好的實(shí)踐是使用TDD(Test-Driven Development,測(cè)試驅(qū)動(dòng)開(kāi)發(fā))的方法,這樣在編寫(xiě)測(cè)試用例的時(shí)候會(huì)很快發(fā)現(xiàn)代碼在可測(cè)試性方面的問(wèn)題。
6. 共享(Re-Usable)
大量的程序?qū)嶋H上都使用了類似的框架或邏輯。由于目前開(kāi)源代碼的大量普及,很多功能并不需要重復(fù)開(kāi)發(fā),只進(jìn)行引用和使用即可。
在一個(gè)組織內(nèi)部,應(yīng)鼓勵(lì)共享和重用代碼,這樣可以有效降低代碼研發(fā)的成本,并提升代碼的質(zhì)量。
實(shí)現(xiàn)代碼的共享,不僅需要在意識(shí)方面提升,還需要具有相關(guān)的能力(如編寫(xiě)?yīng)毩?、高質(zhì)量的代碼庫(kù))及相關(guān)基礎(chǔ)設(shè)施的支持(如代碼搜索、代碼引用機(jī)制)。
7. 可移植(Portable)
某些程序需要在多種操作系統(tǒng)下運(yùn)行,在這種情況下,代碼的可移植性成為一種必需的能力。
要讓代碼具有可移植性,需要對(duì)所運(yùn)行的各種操作系統(tǒng)底層有充分的理解和統(tǒng)一抽象。一般會(huì)使用一個(gè)適配層來(lái)屏蔽操作系統(tǒng)底層的差異。
一些編程語(yǔ)言也提供了多操作系統(tǒng)的可移植性,如很多基于Python語(yǔ)言、Java語(yǔ)言、Go語(yǔ)言編寫(xiě)的程序,都可以跨平臺(tái)運(yùn)行。
8. 可觀測(cè)(Observable) / 可監(jiān)控(Monitorable)
面對(duì)目前大量存在的在線服務(wù)(Online Service)程序,需要具備對(duì)程序的運(yùn)行狀態(tài)進(jìn)行細(xì)致而持續(xù)監(jiān)控的能力。
這要求在程序設(shè)計(jì)時(shí)就提供相關(guān)的機(jī)制,包括程序狀態(tài)的收集、保存和對(duì)外輸出。
9. 可運(yùn)維(Operational)
可運(yùn)維已經(jīng)成為軟件研發(fā)活動(dòng)的重要組成部分,可運(yùn)維重點(diǎn)關(guān)注成本、效率和穩(wěn)定性三個(gè)方面。
程序的可運(yùn)維性和程序的設(shè)計(jì)、編寫(xiě)緊密相關(guān),如果在程序設(shè)計(jì)階段就沒(méi)有考慮可運(yùn)維性,那么程序運(yùn)行的運(yùn)維目標(biāo)則難以達(dá)成。
10. 可擴(kuò)展(Scalable and Extensible)
可擴(kuò)展包含“容量可擴(kuò)展”(Scalable)和“功能可擴(kuò)展”(Extensible)兩方面。
在互聯(lián)網(wǎng)公司的系統(tǒng)設(shè)計(jì)中,“容量可擴(kuò)展”是重要的設(shè)計(jì)目標(biāo)之一。系統(tǒng)要盡量支持通過(guò)增加資源來(lái)實(shí)現(xiàn)容量的線性提高。
快速響應(yīng)需求的變化,是互聯(lián)網(wǎng)公司的另外一個(gè)重要挑戰(zhàn)??煽紤]使用插件式的程序設(shè)計(jì)方式,以容納未來(lái)可能新增的功能,也可考慮使用類似Protocol Buffer 這樣的工具,支持對(duì)協(xié)議新增字段。
以上十條標(biāo)準(zhǔn),如果要記住,可能有些困難。我們可以把它們歸納為四個(gè)方面,見(jiàn)表1。
表1 對(duì)一流代碼特性的匯總分類
壞代碼的例子
關(guān)于好代碼,上面介紹了一些特性,本節(jié)也給出壞代碼(Bad Code)的幾個(gè)例子。關(guān)于壞代碼,本書(shū)沒(méi)有做系統(tǒng)性總結(jié),只是希望通過(guò)以下這些例子的展示讓讀者對(duì)壞代碼有直觀的感覺(jué)。
1. 不好的函數(shù)名稱(Bad Function Name)
如do(),這樣的函數(shù)名稱沒(méi)有多少信息量;又如myFunc(),這樣的函數(shù)名稱,個(gè)人色彩過(guò)于強(qiáng)烈,也沒(méi)有足夠的信息量。
2. 不好的變量名稱(Bad Variable Name)
如a、b、c、i、j、k、temp,這樣的變量名稱在很多教科書(shū)中經(jīng)常出現(xiàn),很多人在上學(xué)期間寫(xiě)代碼時(shí)也會(huì)經(jīng)常這樣用。如果作為局部變量,這樣的名稱有時(shí)是可以接受的;但如果作為作用域稍微大的變量,這樣的名稱就非常不可取了。
3. 沒(méi)有注釋(No Comments)
有寫(xiě)注釋習(xí)慣的軟件工程師很少,很多軟件工程師認(rèn)為寫(xiě)注釋是浪費(fèi)時(shí)間,是“額外”的工作。但是沒(méi)有注釋的代碼,閱讀的成本會(huì)比較高。
4. 函數(shù)不是單一目的(The Function has No Single Purpose)
如LoadFromFileAndCalculate()。這個(gè)例子是我編造的,但現(xiàn)實(shí)中這樣的函數(shù)其實(shí)不少。很多函數(shù)在首次寫(xiě)出來(lái)的時(shí)候,就很難表述清楚其用途;還有一些函數(shù)隨著功能的擴(kuò)展,變得越來(lái)越龐雜,也就慢慢地說(shuō)不清它的目的了。
這方面的問(wèn)題可能很多人都沒(méi)有充分地認(rèn)識(shí)到——非單一目的的函數(shù)難以維護(hù),也難以復(fù)用。
5. 不好的排版(Bad Layout)
不少人認(rèn)為,程序可以正常執(zhí)行就行了,所以一些軟件工程師不重視對(duì)代碼的排版,認(rèn)為這僅僅是一種“形式”。
沒(méi)有排好版的程序,在閱讀效率方面會(huì)帶來(lái)嚴(yán)重問(wèn)題。這里舉一個(gè)極端的例子:對(duì)于C語(yǔ)言來(lái)說(shuō),“;”可作為語(yǔ)句的分割符,而“縮進(jìn)”和“換行”對(duì)于編譯器來(lái)說(shuō)是無(wú)用的,所以完全可以把一段C語(yǔ)言程序都“壓縮”在一行內(nèi)。這樣的程序是可以運(yùn)行的,但是對(duì)人來(lái)說(shuō),可讀性非常差。這樣的程序肯定是我們非常不希望看到的。
6. 無(wú)法測(cè)試(None Testable)
程序的正確性要依賴測(cè)試來(lái)保證(雖然測(cè)試并不能保證程序完全無(wú)錯(cuò))。無(wú)法或不好為之編寫(xiě)測(cè)試用例的程序,是很難有質(zhì)量保證的。
好代碼從哪里來(lái)
上一節(jié)說(shuō)明了好代碼的特性,本節(jié)來(lái)分析好代碼是如何產(chǎn)出的。
好代碼不止于編碼
好代碼從哪里來(lái)?
對(duì)于這個(gè)問(wèn)題,很多讀者肯定會(huì)說(shuō):“好代碼肯定是寫(xiě)出來(lái)的呀?!?
我曾做過(guò)多次調(diào)研,發(fā)現(xiàn)很多軟件工程師日常所讀的書(shū)確實(shí)是和“寫(xiě)代碼”緊密相關(guān)的。
但是,這里要告訴讀者的是,代碼不只是“寫(xiě)”出來(lái)的。在很多年前,我所讀的軟件工程方面的教科書(shū)就告訴我,編碼的時(shí)間一般只占一個(gè)項(xiàng)目所花時(shí)間的 10%。我曾說(shuō)過(guò)一句比較有趣的話:
“如果一個(gè)從業(yè)者告訴你,他的大部分時(shí)間都在寫(xiě)代碼,那么他大概率不是一個(gè)高級(jí)軟件工程師。”
那么,軟件工程師的時(shí)間都花到哪里去了呢?軟件工程師的時(shí)間應(yīng)該花在哪里呢?
好的代碼是多個(gè)工作環(huán)節(jié)的綜合結(jié)果。
(1)在編碼前,需要做好需求分析和系統(tǒng)設(shè)計(jì)。而這兩項(xiàng)工作是經(jīng)常被大量軟件工程師忽略或輕視的環(huán)節(jié)。
(2)在編碼時(shí),需要編寫(xiě)代碼和編寫(xiě)單元測(cè)試。對(duì)于“編寫(xiě)代碼”,讀者都了解;而對(duì)于“編寫(xiě)單元測(cè)試”,有些軟件工程師就不認(rèn)同了,甚至還有人誤以為單元測(cè)試是由測(cè)試工程師來(lái)編寫(xiě)的。
(3)在編碼后,要做集成測(cè)試、上線,以及持續(xù)運(yùn)營(yíng)/迭代改進(jìn)。這幾件事情都是要花費(fèi)不少精力的,比如上線,不僅僅要做程序部署,而且要考慮程序是如何被監(jiān)控的。有時(shí),為了一段程序的上線,設(shè)計(jì)和實(shí)施監(jiān)控的方案要花費(fèi)好幾天才能完成。
因此,一個(gè)好的系統(tǒng)或產(chǎn)品是以上這些環(huán)節(jié)持續(xù)循環(huán)執(zhí)行的結(jié)果。
需求分析和系統(tǒng)設(shè)計(jì)
1. 幾種常見(jiàn)的錯(cuò)誤現(xiàn)象
相對(duì)于編碼工作,需求分析和系統(tǒng)設(shè)計(jì)是兩個(gè)經(jīng)常被忽視的環(huán)節(jié)。在現(xiàn)實(shí)工作中,我們經(jīng)常會(huì)看到以下這些現(xiàn)象。
(1)很多人錯(cuò)誤地認(rèn)為,寫(xiě)代碼才是最重要的事情。不少軟件工程師如果一天沒(méi)有寫(xiě)出幾行代碼,就會(huì)認(rèn)為工作沒(méi)有進(jìn)展;很多管理者也會(huì)以代碼的產(chǎn)出量作為衡量工作結(jié)果的主要標(biāo)準(zhǔn),催促軟件工程師盡早開(kāi)始寫(xiě)代碼。
(2)有太多的從業(yè)者,在沒(méi)有搞清楚項(xiàng)目目標(biāo)之前就已經(jīng)開(kāi)始編碼了。在很多時(shí)候,項(xiàng)目目標(biāo)都是通過(guò)并不準(zhǔn)確的口頭溝通來(lái)確定的。例如:
“需要做什么?”
“就按照×××網(wǎng)站的做一個(gè)吧?!?
(3)有太多的從業(yè)者,在代碼編寫(xiě)基本完成后,才發(fā)現(xiàn)設(shè)計(jì)思路是有問(wèn)題的。他們?cè)诤芏囗?xiàng)目上花費(fèi)很少(甚至沒(méi)有花費(fèi))時(shí)間進(jìn)行系統(tǒng)設(shè)計(jì),對(duì)于在設(shè)計(jì)中所隱藏的問(wèn)題并沒(méi)有仔細(xì)思考和求證?;谶@樣的設(shè)計(jì)投入和設(shè)計(jì)質(zhì)量,項(xiàng)目出現(xiàn)設(shè)計(jì)失誤也是很難避免的。而面對(duì)一個(gè)已經(jīng)完成了基本編碼的項(xiàng)目,如果要“動(dòng)大手術(shù)”來(lái)修改它,相信每個(gè)有過(guò)類似經(jīng)歷的人都一定深知那種感受——越改越亂,越改越著急。
以上這幾種情況,很多讀者是不是都有過(guò)類似經(jīng)歷?
2. 研發(fā)前期多投入,收益更大
關(guān)于軟件研發(fā),首先我們需要建立一個(gè)非常重要的觀念。
在研發(fā)前期(需求分析和系統(tǒng)設(shè)計(jì))多投入資源,相對(duì)于把資源都投入在研發(fā)后期(編碼、測(cè)試等),其收益更大。
這是為什么呢?
要回答這個(gè)問(wèn)題,需要從軟件研發(fā)全生命周期的角度來(lái)考量軟件研發(fā)的成本。除編碼外,軟件測(cè)試、上線、調(diào)試等都需要很高成本。如果我們把需求搞錯(cuò)了,那么與錯(cuò)誤需求有關(guān)的設(shè)計(jì)、編碼、測(cè)試、上線等成本就都浪費(fèi)了;如果我們把設(shè)計(jì)搞錯(cuò)了,那么與錯(cuò)誤設(shè)計(jì)相關(guān)的編碼、測(cè)試、上線的成本也就浪費(fèi)了。
如果仔細(xì)考量那些低效的項(xiàng)目,會(huì)發(fā)現(xiàn)有非常多的類似于上面提到的“浪費(fèi)”的地方。軟件工程師似乎都很忙,但是在錯(cuò)誤方向上所做的所有努力并不會(huì)產(chǎn)生任何價(jià)值,而大部分的加班實(shí)際上是在做錯(cuò)誤的事情,或者是為了補(bǔ)救錯(cuò)誤而努力。在這種情況下,將更多的資源和注意力向研發(fā)前期傾斜會(huì)立刻收到良好的效果。
3. 修改代碼和修改文檔,哪個(gè)成本更高
很多軟件工程師不愿意做需求分析和系統(tǒng)設(shè)計(jì),是因?yàn)閷?duì)“寫(xiě)文檔”有著根深蒂固的偏見(jiàn)。這里問(wèn)大家一個(gè)問(wèn)題,如果大家對(duì)這個(gè)問(wèn)題能給出正確的回答,那么在“寫(xiě)文檔”的意識(shí)方面,一定會(huì)有很大的轉(zhuǎn)變。
任何人都不是神仙,無(wú)法一次就把所有事情做對(duì)。對(duì)于一段程序來(lái)說(shuō),它一定要經(jīng)過(guò)一定周期的修改和迭代。這時(shí)有兩種選擇:
選擇一:修改文檔。在設(shè)計(jì)文檔時(shí)完成迭代調(diào)整,待沒(méi)有大問(wèn)題后再開(kāi)始編碼。
選擇二:修改代碼。只有粗略的設(shè)計(jì)文檔,或者沒(méi)有設(shè)計(jì)文檔,直接開(kāi)始編碼,所有的迭代調(diào)整都在代碼上完成。
請(qǐng)大家判斷,修改代碼和修改文檔,哪個(gè)成本更高?
在之前的一些分享交流會(huì)上,對(duì)于這個(gè)問(wèn)題,有人會(huì)說(shuō),修改文檔的成本更高。因?yàn)樵谛薷奈臋n后還要修改代碼,多了一道手續(xù)。而直接修改代碼,只需要做一次,這樣更直接。
這個(gè)回答說(shuō)明了回答者沒(méi)有充分理解“先寫(xiě)文檔,后寫(xiě)代碼”的設(shè)計(jì)方法。如果沒(méi)有充分重視設(shè)計(jì)文檔的工作,在輸出的設(shè)計(jì)文檔質(zhì)量不高的情況下就開(kāi)始編碼,確實(shí)會(huì)出現(xiàn)以上提到的問(wèn)題。但是,如果在設(shè)計(jì)文檔階段就已經(jīng)做了充分考慮,會(huì)減少對(duì)代碼的迭代和反復(fù)。
對(duì)于同樣的設(shè)計(jì)修改,“修改代碼”的成本遠(yuǎn)高于“修改文檔”。這是因?yàn)?,在設(shè)計(jì)文檔中只會(huì)涉及主要的邏輯,那些細(xì)小的、顯而易見(jiàn)的邏輯不會(huì)在設(shè)計(jì)文檔中出現(xiàn)。在修改設(shè)計(jì)文檔時(shí),也只會(huì)影響到這些主要邏輯。而如果在代碼中做修改,不僅會(huì)涉及這些主要邏輯,而且會(huì)涉及那些在文檔中不會(huì)出現(xiàn)的細(xì)小邏輯。對(duì)于一段程序來(lái)說(shuō),任何一個(gè)邏輯出現(xiàn)問(wèn)題,程序都是無(wú)法正常運(yùn)行的。
4. 需求分析和系統(tǒng)設(shè)計(jì)之間的差別
很多讀者無(wú)法清楚地區(qū)分“需求分析”和“系統(tǒng)設(shè)計(jì)”之間的差別,于是會(huì)發(fā)現(xiàn),在寫(xiě)出的文檔中,有些需求分析文檔里出現(xiàn)了系統(tǒng)設(shè)計(jì)的內(nèi)容,而有些系統(tǒng)設(shè)計(jì)文檔里又混雜了需求分析的內(nèi)容。
我們用幾句話可以非常明確地給出二者的差異。
(1)需求分析:定義系統(tǒng)/軟件的黑盒的行為,它是從外部(External)看到的,在說(shuō)明“是什么”(What)。
(2)系統(tǒng)設(shè)計(jì):設(shè)計(jì)系統(tǒng)/軟件的白盒的機(jī)制,它是從內(nèi)部(Internal)看到的,要說(shuō)明“怎么做”(How)和“為什么”(Why)。
比如,對(duì)一輛汽車來(lái)說(shuō),首先使用者從外部可以看到車廂、車輪,坐在車?yán)锟梢钥吹椒较虮P(pán)、剎車踏板、油門踏板等;操作方向盤(pán)可以改變汽車的行駛方向,腳踩剎車踏板、油門踏板可用于減速和加速。以上這些是對(duì)汽車的“需求分析”。
然后,我們想象汽車外殼和內(nèi)部變成了透明的,可以看到汽車內(nèi)部的發(fā)動(dòng)機(jī)、變速箱、傳動(dòng)桿、與剎車相關(guān)的內(nèi)部裝置等。而這些對(duì)駕駛者來(lái)說(shuō)是不可見(jiàn)的,它們是對(duì)汽車的“系統(tǒng)設(shè)計(jì)”。