如何寫好代碼 二、爲什麼要寫好代碼 三、何爲好代碼 四、寫好代碼的方法 五、參考

  • 行業發展迅速
  • 技術發展迅速
  • 代碼編寫本身的難度

二、爲什麼要寫好代碼

從公司角度講,現在互聯網已經進入到一個相對成熟理性的階段,很多一二線互聯網公司成立的時間都超過了十年。現在各家公司的發展方式都逐漸從單純注重速度轉變爲速度與質量兼顧。軟件的質量,尤其是關鍵系統、核心系統的軟件質量,對於各家公司的重要性不言而喻。最近的一個忽視軟件質量的例子 —— 波音公司,其後果我想大家都是知道的。

互聯網公司的軟件質量如果出問題,通常不至性命關天,但大筆真金白銀和用戶口碑的損失也是擔受不起。

從程序員角度講,代碼編寫能力就有如武林中人的內功般重要。內功會決定你的發展高度。即便日後做了技術管理崗的工作,如有紮實的代碼編寫能力,也會助你做好管理工作。因爲技術人員總有這麼一個特點,外行是不容易管好內行的。如果你打算長期在技術線上發展,那代碼編寫能力就更加重要。

從管理角度講,如果你是一個技術團隊的直接負責人,那你需要重視你團隊成員的編碼質量,因爲這直接關乎你所負責項目的質量、團隊的研發效率、業務的發展。如果你是更高等級的 Leader,你的直接下屬是其它 Leader。雖然你通常無需關心的技術細節,但編碼質量還是會對前面幾個問題產生影響。更重要的是,對於一個技術部門,或者是一個以技術爲驅動力的公司而言,管理層需要從整個公司技術人員和技術團隊長期良性發展的角度考慮問題,代碼質量等技術細節如果長期不被重視,會對技術團隊的穩定性等方面產生負面影響。

三、何爲好代碼

如同想成爲好作者就要先學會識好書一樣,想成爲優秀的程序員就要知道什麼樣的代碼是好代碼。個人認爲,判斷一段代碼的好壞,要從可讀性和可擴展性兩點入手。

3.1 可讀性

可讀性是大家談到好代碼時往往會第一個想到的。什麼是可讀性?顧名思義,就是指代碼易於閱讀和理解的程度。一些大牛所說過的關於何爲好代碼的名言,往往都是從可讀性的角度表述的:

比如 ThoughtWorks 的首席科學家,《重構》一書的作者 Martin Fowler 說過:

任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,纔是優秀的程序員。

IBM 的首席軟件工程科學家 Grady Booch 說過:

整潔的代碼如同優美的散文。

換言之,代碼是給人看的,而不是給計算機看的。可讀性好的代碼就如同一篇優秀的說明書,第一步做什麼、第二步再做什麼,都應有清晰的描述;可讀性好的代碼要有恰如其分、名副其實的命名;可讀性好的代碼要有合適的格式和組織,哪些代碼在前,哪些代碼在後,哪裏需要縮進,哪裏需要空行。這些都是有嚴格規範的。

總之,對於代碼可讀性,其實

3.2 可擴展性

但可讀性好的代碼也不一定會是優秀的代碼。優秀的代碼還應具有良好的可擴展性。

可擴展性指的是代碼易於擴展功能的程度。軟件行業是個變化迅速的行業,互聯網更是如此。面對迅速的變化,擴展性的重要便體現了出來。可讀性好的代碼,程序員易於修改,從而易於擴展功能。但這往往還不夠。可擴展性往往追求的是在不修改原有代碼的情況下去擴展功能。即軟件設計原則中的開閉原則。

不過很多時候,代碼的可讀性和可擴展性是有一定程度的相互矛盾。如果大家閱讀過一些開源軟件的源碼,對這一點就會有體會。這些開源軟件的代碼質量通常都不錯,但讀懂卻不是那麼容易。背後的原因除了你需要具備對應領域的知識以外,更多的就是因爲可擴展性所引入的複雜設計一定程度上降低了可讀性。

但在這種情況下,可讀性的稍微降低並不代表這個軟件的代碼不優秀。優秀但卻複雜的代碼,往往會有詳盡的文檔和註釋,代碼設計和編寫上往往能讓閱讀者有章可循。並且從表及裏呈現出層層遞進形式,使閱讀者即可瞭解大意和結構,也可逐漸深入,瞭解細節。這一點同優秀的書籍類似。

3.3 何爲爛代碼

判斷何爲好代碼,也可以從另一個角度進行,那就是判斷何爲爛代碼。爛代碼的特性通常被稱爲代碼的壞味道。壞味道在《重構》一書中有詳細討論,這裏我只簡單說幾個:

  • 代碼重複
  • 方法過長、類過長、參數過長
  • 過多的、嵌套過深的 if...else 或 switch
  • 分散式變化、霰彈式修改、依賴情結

代碼重複

編程界的另一位大神 Martin 叔叔說過:

重複可能是軟件中一切邪惡的根源。—— Robert C.Martin

所以說代碼重複可以說是頭號壞味道,原因是重複代碼會大幅增加代碼維護成本,也是各種 Bug 潛在的溫牀。現在各種集成開發環境和代碼檢查工具都有重複代碼檢查功能,可以大大降低重複代碼發現成本,可以幫助開發者及時消除重複代碼。

除了工具可發現的重複代碼,在項目中可能還會有很多需要程序員仔細觀察才能發現的重複代碼。這些重複代碼往往是由原來簡單的重複代碼演變而來,並且具有更大的隱蔽性和危害性。這也說明了重複代碼需要及時修復。

不過現在流行的微服務架構,會在一定程度上增加代碼重複程度(有些同學可能對此沒有體會,詳細,微服務做的多了就能理解了),而且因爲這些重複的代碼是跨系統、跨項目的,傳統的工具無法發現。

方法過長、類過長、參數過長

通常而言,過長的方法、類和參數都意味着這段代碼是一段糟糕的代碼。那麼多長算長呢?以 Java 爲例,一個方法長度不應超過50行,一個類不應超過1000行,最好不超過500行,方法參數不超過4個。

這些只是建議,不應該一刀切地判斷。因爲對於一個複雜算法或技術的實現,過於控制方法、類和參數的長度是不適宜的,因爲對於這些算法技術本身的理解其實遠超過理解代碼實現的難度。但是,這不能作爲普通程序員對自己代碼的長度不加控制的理由,畢竟多數人寫的代碼所要表達的邏輯都是很容易理解的。

方法和類過長通常都說明這段代碼違反了單一職責原則。參數過長同樣如此,通常都是一個方法關心了太多不該它關心的事情所致,也有些是由於所有參數平鋪所致。

過多的、嵌套過深的 if...else 或 switch

過於複雜的條件語句是另一種很明顯的代碼壞味道。對於這一點,我想我不必做過多解釋,寫過代碼的應該都懂。

對於如何解決複雜條件語句這個問題,我寫過專門的文章 —— 《如何“幹掉”if...else》https://www.jianshu.com/p/1db0bba283f0 。因此這裏我就不再贅述。

分散式變化、霰彈式修改、依賴情結

這三點壞味道不如之前的容易理解。這裏先一句話介紹這三個壞味道的含義(在《重構》一書中有詳細解釋):

  • 分散式變化:一個類常因爲不同原因而進行修改
  • 霰彈式修改:多個類常因爲相同原因而進行修改
  • 依賴情結:一個方法對其它類的興趣高過自己所屬類的興趣

看過一句話介紹之後相信還會有很多同學不理解,再詳細介紹一下。分散式變化常反映出一個類(或方法)不滿足單一職責原則。它做的事太多,纔會導致各種原因的變化都會帶來對它的修改。霰彈式修改則與分散式變化相對,它反映的是軟件設計的另一個問題:低內聚。一個功能,分散的到處都是,這樣通常就會導致一個需求變化需要到處修改。

從上面的介紹也能看出,軟件設計的複雜性。很多原則其實相互矛盾,就像單一職責和內聚性。軟件工程師需要在設計時平衡這些相互矛盾的原則,才能設計出優秀的軟件。

依賴情結,雖然字面上不容易理解,但是在日常工作中體現的其實更多。經常能看到這樣的方法:它從一個或幾個類中取出數據,然後經過處理,然後設置到另一個類中。這個方法從始至終都沒有使用過自己類的屬性。如果是靜態方法,通常也無可厚非(畢竟靜態方法不能訪問自己類的屬性)。可是我們更常見到的都實例方法。這其實反映出一個事實:定義這個方法的位置錯了。

小結一下:

  • 分散式變化反映軟件設計違反了單一職責
  • 霰彈式修改反映出軟件設計的不夠內聚
  • 依賴情結反映出方法放錯了位置

四、寫好代碼的方法

寫好代碼應該是各級別程序員共同的目標。換言之,寫好代碼就是程序員的自我修養。

但不同級別的程序員,寫好代碼這件事其實有不同的要求。

對於普通的程序員,更多的精力應該放在如何提高代碼可讀性爲主要的目標。即努力把代碼寫的清楚、寫的明白。這裏涉及到的技術通常是代碼編寫的一些基本規範、技巧、簡單的代碼重構手段,可能還包括面向對象方面的知識。

重點說明的是,我並沒有提及各種軟件設計方面的原則,比如單一職責、開閉原則。原因在於,所謂原則,就是一些你看似明白,實則不懂的東西。掌握原則,需要多加練習和思考。

而對於高級和資深的工程師,應具備編寫兼具可讀性和可擴展性的代碼。這裏還需再次強調,可讀性和可擴展性有時是矛盾的。因此,這一階段的程序員需要能平衡好可讀性和可擴展性。同時也需要能從工程和業務的角度考慮,代碼要避免過度設計,但也不能不考慮擴展。

所以,編寫可擴展性高的代碼,除了需要具備熟練掌握各種設計模式、設計原則和思想、重構手段等等。還需要開發者對所在業務領域有深入理解,從而在何時的地方做出具有合適擴展能力的設計。

接下來說幾個簡單的提高代碼質量的方法。

4.1 命名

第一個想強調的代碼的命名。命名是一個不被人重視的編碼細節,但能夠爲代碼、軟件起一個簡單明瞭、恰如其分的名字其實是非常有價值的,而且也不是一個簡單的事情。

試想一下,如果你有了孩子,是不是需要仔細考慮孩子的姓名?如果隨便起個張三李四,那是一定不是一個稱職的父母。同樣,對於代碼,你隨便起個名字,那同樣也是不負責任的表現。

命名並不是簡單想幾個單詞並拼接在一起而已。命名其實反映了開發者對業務理解的程度和軟件設計的能力。一個好名字實際是對一個業務功能簡短而又精準的表述,其背後體現了開發者對代碼規範、面向對象設計、設計原則、設計模式,甚至架構設計等能力的掌握和運用的好壞。

方法命名

方法命名的一個原則是解釋目的,而不是手段。即方法命名只需說明這個方法是幹什麼的即可,不用通過方法命名體現這個方法是如何做的。

方法命名的一般格式是:動詞+名詞短語+(額外修飾)。

例如,在 Spring 的 BeanFactory 接口中有如下方法定義:

Object getBean(String name)

這個方法命名就是動詞+名詞的形式,因爲方法功能比較簡單,所以沒有加額外修飾。

有時我們能看到方法名稱體現了內部實現方式。假如,我們需要實現一個分佈式的 Spring,Bean 的定義存在 Redis 裏(實際顯然沒有這個必要,這裏只是舉個多數人容易理解的例子)。那估計 getBean 這個方法就會有人定義成如下形式:

Object getBeanInRedis(String name)

這時,InRedis 體現就是方法內部實現方法。這麼做是多餘的,即違反了簡單的原則,也違反了方法命名體現目的,而非方法的原則(另外也非常的不面向對象。如果你真的想實現一個基於 Redis 的 Spring,可以創建一個 BeanFactory 的實現類 —— RedisBeanFactory)

其實上述方法命名有時還不夠簡單。例如在 Spring Data 的 Repository 定義中,我們能看到如下方法:

  • save
  • saveAll
  • findById
  • findAll

這些方法的命名簡單到連名詞部分也省略了。原因在於 Repository 接口的實現(如 OrderRepository)中已經包含了這些方法所操作的對象,所以也就不用重複了。在面嚮對象語言中,方法調用通常都是 object(class).method(args) 的形式。這時,object 或 class 的命名應該反映出一些業務含義,這些含義不必在方法命名中重複表現。

在非面嚮對象語言中,道理同樣存在。如在 Golang 中有這樣的方法 time.Parse(layout, value string)。這裏的 time 是包名,但在命名上起到作用同面嚮對象語言的對象和類是一樣的。

剛看到了一些簡單的方法命名的例子,接下來看一些複雜的命名:

startEventDispatchThreadIfNecessary

上面這個例子是 JDK 中的一個方法。這個方法的命名很長,翻譯過來就是“啓動時間分發線程,如果必要”。前半句好理解,那爲什麼後面要加上一句“如果必要”呢?原因在於如果不加,其他開發者會誤以爲調用這個方法一定會啓動一個事件分發線程,但實際情況是有些情況下不會這麼做。那什麼情況下不會這麼做呢?這算是一個細節,一般情況下不用在方法命名上體現,否則方法名就太長了。如果這個細節確屬必要,那可以通過註釋來描述。

方法命名中帶有 IfXxx 例子還有很多,在各種開源軟件的源碼中都能找到。這裏想要說的是,爲了達到讓使用者正確理解一個方法所要達到的目的,有時需要在動詞+名詞的命名形式之上再增加額外的描述。

變量命名

對於變量的命名,它的作用主要有兩點:一是描述對象(或數據結構)所具有的屬性;二是對方法執行過程進行輔助性描述。

接下來我將介紹一些代碼命名的基本規則,以及幾個例子。

對於變量命名的第一點作用,很容易理解。因爲對於面嚮對象語言來說,一個類就是數據和行爲的封裝,而數據其實就是對象的屬性。對於非面嚮對象語言,如 C、Go,它們的結構體也包含有數據(雖然不能定義方法,沒有行爲)。

對於變量命名的第二點作用,多解釋一下。這一點作用通常是對局部變量而言的。在一個方法體中,另一個方法的返回值需要被使用多次使用,這時最好使用臨時變量保存這個方法的返回值。這很容易理解。如果只是用一次呢?其實有時也需一個臨時變量。這個臨時變量的作用通常爲了更好的解釋這個值的目的和含義。

舉例來說:

List<Order> paidOrders = findAllByStatus(OrderStatus.PAID);

這時,paidOrders 顯然比 findAllByStatus(OrderStatus.PAID) 更容易理解,也更簡短。

理解了變量的作用之後,如何命名也就清楚了。畢竟命名的目的在於用更簡單的方式描述作用。

所有,對於下面的例子,哪種命名更好呢?

class Order {
    private String name;
    private String orderName;
}

顯然,name 更好。雖然 orderName 也能體現“訂單名稱”這個作用,但是前者更簡單。

在 Java 語言中,類是第一類公民,也是編程者遇到的第一個需要命名的東西。類的基本命名規則通常爲形容詞+名詞的形式,最後一部分的名詞詞組表示的是這個類所表示的是哪一類事物。如果一個接口只有一種實現類,通常可將這個類命名爲接口名+Impl,這也是被廣泛接受的命名形式。

類的背後其實體現的是面向對象的設計(看到這裏我相信會有很多人對面向對象嗤之以鼻。確實,以 Java 爲代表的面向對象編程語言不如函數式的語言簡單靈活。但請相信,在更好的方法出現之前,面向對象的設計方法是應對複雜業務邏輯最好的方法。同時,Java 使用過程中出現的很多問題,實則是開發者沒有理解好面向對象設計所導致的)

面向對象設計,看似簡單,但其實需要對業務領域的深刻理解。在這方面,領域驅動設計是一個非常好的指南,它能夠指導如何設計一個業務系統,自然也能夠指導如何命名。

但僅僅將類命名成某實體類、某 Factory、某 Repository、某 Service 是遠遠不夠的。除此之外,能夠指導我們命名類(也包括接口)的是各種設計模式。比如某某 Builder、某某 Strategy、某某 Command。當然,也沒必要死抱着設計模式,因爲設計模式體現的是實現方法,而這一點通常不是命名首要考慮的問題(命名首要考慮的是目的)。

在類的命名上,常見的一個具體問題是 Service 和 Manager 這兩種命名隨意使用。表面上這兩者都是用來實現務邏輯的組件,但還是有些區別。一般來說,Service 通常是無狀態的業務組件,而 Manager 通常爲有狀態的。

小結一下:

  • 類命名是面向對象設計的體現
  • 業務系統的類命名可參考領域驅動設計
  • 其它領域的類命名可參考設計模式
  • 不用死抱上述建議,只要命名體現類設計的目的即可

接口

不同於類所表示的具體的概念,接口表示的是泛化的概念。接口通常表示一類事物,或一類事物所共有的特性。同類和變量的基本命名規則一樣,接口的命名通常也是名詞形式。例如,Spring Framework 中的 ApplicaitonContextBeanFactoryInitializingBean 等接口。但我們有時能看到一些代碼在接口前加 I 這個前綴,以表示這個是一個接口,而非一個類。這種風格是非常不推薦的。開發者應當把接口看作是更通用的一個概念,而非特殊概念。因此,不應在命名前加增 I 這個前綴,因爲增加前綴是一種特殊化的做法。比如,ApplicationContext 是一個好的接口命名的實例,但 IApplicationContext 就不是。少數情況下,增加前綴還會導致歧義,比方說,沒人會把 IPhone 理解爲是電話這個概念的接口。

除了名詞形式的命名,接口還有另一類命名方式 —— 形容詞命名。比如,在 JDK 中,我們常見的有 SerializableCloneableComparableRunnable,其它開源項目中這樣的命名方式也有很多,就不一一列舉了。這種風格的命名所表示的都是一種特性 —— 能做什麼。

其它接口命名實例還有 XxxAware,這在 Spring Framework 中比較常見。

小結一下,接口命名的方法主要體現了接口的兩類作用:

  • 表示一類事物:名詞形式接口命名
  • 表示某種特性:形容詞形式接口命名

4.2 一些提高編碼能力的“旁門左道”

重複造輪子

重複造輪子通常都是編程界的貶義詞,但在今天這個話題裏,我認爲“重複造輪子”是褒義詞。這裏我們重複造輪子的目的是通過模仿現有開源技術提高自己的編程能力。提高編程能力沒有捷徑,最終看的就是代碼編寫量,還要是高質量的代碼編寫量。在日常工作中,允許你對代碼精打細磨的機會並不多,這時你就需要尋找額外“訓練”機會。研究開源技術源碼,嘗試重寫,或者更進一步,爲開源技術貢獻代碼,能讓你的編碼能力提高很多。

結對編程

結對編程是敏捷開發中所提到的一個工程實踐。不過似乎在國內公司中實踐的較少(我在外企和互聯網行業工作時實踐過一些結對編程)。

結對編程有很多好處,在提高編碼質量方面,因爲結對編程通常一人寫一人看,或一人寫實現一人寫單測。因此,你的代碼不僅需要自己理解,至少還需要你的同伴理解。而且因爲這一閱讀理解的過程是實時進行的,這就使得對代碼的 Review 非常細粒度,這也促使你的代碼質量的提高。

單元測試

要寫好代碼就少不了修改代碼,那如何對已正確實現業務功能的代碼進行修改還保證不出錯呢?這就需要單元測試。測試可以是軟件工程中最重要的環節之一,重要性不亞於開發。而單元測試,應當是測試粒度最細,也是與開發人員距離最近的測試形式。如果一個項目沒有任何單元測試,基本可以斷定它不會是一個好項目。

單元測試可說是寫好代碼的前提,因此要想把代碼寫好的同學已經要掌握編寫單元測試的技術。可能有同學覺得寫單元測試就是 JUnit(對於其它語言也有類似框架或有內置單元測試支持)。如果你這麼想,那你就是太 naive 了。我面試時問過十幾個程序員關於單元測試的問題,沒聽說過值校驗和行爲校驗的一個沒有。可能這個問題有些偏門,答不出來可以理解。那單測中的 Mock 技術都有作用?Mock 和 Stub 的區別?能答出來也沒有。這些問題表面上是概念性的問題,但其實能反映一個技術人員對單元測試技術的實際經驗的多少。

單元測試的不易的另一個體現在於單元測試的兩個矛盾。第一個矛盾是單元測試本身也是代碼,開發人員編碼質量的好壞也會影響單元測試代碼。單元測試寫不好,最終會導致別人無法理解測試用例的含義,也會對整個項目的維護性造成很大的負面影響。另外就是單元測試如果覆蓋完整的話,實際的代碼量會比被測代碼本身還多。如何把單元測試寫的精簡、易於理解、覆蓋完整也是一個頗有技術含量的工作。

單元測試的第二個矛盾是對於某些代碼質量不高的項目來說,補充單元測試是一個很有挑戰的工作。但是不補充單元測試項目的代碼重構又很難保證質量。不重構又難以提高代碼質量。。。看到沒,這就是個死循環:代碼質量差 -> 難以單測 -> 代碼質量差。筆者之前所在那個外企項目就死在這一點上了。

所以,早寫單測。

少動鼠標

用好各種開發工具,如 VIM、IDEA 的快捷鍵,以及各種命令行工具,儘量少用鼠標。這麼做不一定成爲編程高手,但編程高手都能這麼玩。如同競技遊戲中,哪個魔獸、星際、DOTA 高手用鼠標放技能呢?編程同理。

五、參考

  • 《重構 - 改善既有代碼的設計》by Martin Fowler
  • 《代碼整潔之道》by Robert C. Martin
  • 《重構與模式》by Joshua Kerievsky
  • 《實現模式》by Kent Beck
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章