> 原文出處:http://weibo.com/p/1001643877361430185536
> 作者:秦迪,[@蛋疼的AXB](http://weibo.com/n/%E8%9B%8B%E7%96%BC%E7%9A%84AXB)

**課程大綱**
[TOC=2,2]
## 什么是好代碼
對于代碼質量的定義需要于從兩個維度分析:主觀的,被人類理解的部分;還有客觀的,在計算機里運行的狀況。
我把代碼質量分為五個層次,依次為:
* 完成功能的代碼
* 高性能的代碼
* 易讀的代碼
* 可測試的代碼
* 可擴展的代碼
## 如何編寫可讀的代碼
在很多跟代碼質量有關的書里都強調了一個觀點:程序首先是給人看的,其次才是能被機器執行。
### 逐字翻譯
在評價一段代碼能不能讓人看懂的時候,可以自己把這段代碼**逐字**翻譯成中文,試著組成句子,之后把中文句子讀給另一個人沒有看過這段代碼的人聽,如果另一個人能聽懂,那么這段代碼的可讀性基本就合格了。
而實際閱讀代碼時,讀者也會一個詞一個詞的閱讀,推斷這句話的意思,如果僅靠句子無法理解,那么就需要聯系上下文理解這句代碼,如果簡單的聯系上下文也理解不了,可能還要掌握更多其它部分的細節來幫助推斷。大部分情況下,理解一句代碼**在做什么**需要聯系的上下文越多,意味著代碼的質量越差。
逐字翻譯的好處是能讓作者能輕易的發現那些只有自己知道的、沒有體現在代碼里的假設和可讀性陷阱。無法從字面意義上翻譯出原本意思的代碼大多都是爛代碼,比如“ms代表messageService“,或者“ms.proc()是發消息“,或者“tmp代表當前的文件”。
### 遵循約定
約定包括代碼和文檔如何組織,注釋如何編寫,編碼風格的約定等等,這對于代碼未來的維護很重要。
大家剛開始工作時,一般需要與部門的約定保持一致,包括一些強制的規定,如代碼的格式化設置文件;或者一些非強制的約定,如工程的命名等。
從更大的范圍考慮,整個行業的約定和規則也在不斷的更新。所以在工作中也要對行業動向和開源項目保持關注,如果發現部門中哪一項約定已經過時了,那么可以隨時提出來,由平臺的架構師小組review之后推進平臺更新這些規則。
### 文檔和注釋
對于文檔的標準很簡單,能找到、能讀懂就可以了,一般一個工程至少要包含以下幾類文檔:
1. 對于項目的介紹,包括項目功能、作者、目錄結構等,讀者應該能3分鐘內大致理解這個工程是做什么的。
2. 針對新人的QuickStart,讀者按照文檔說明應該能在1小時內完成代碼構建和簡單使用。
3. 針對使用者的詳細說明文檔,比如接口定義、參數含義、設計等,讀者能通過文檔了解這些功能(或接口)的使用方法。
有一部分注釋實際是文檔,比如javadoc。這樣能把源碼和注釋放在一起,對于讀者更清晰,也能簡化不少文檔的維護的工作。
還有一類注釋并不作為文檔的一部分,比如函數內部的注釋,這類注釋的職責是說明一些代碼本身無法表達的作者在編碼時的思考,比如“為什么這里沒有做XX”,或者“這里要注意XX問題”。
一般來說函數內部注釋的數量應該不會有很多,也不會完全沒有,一般滾動幾屏幕看到一兩處左右比較正常。過多的話可能意味著代碼本身的可讀性有問題,而如果一點都沒有可能意味著有些隱藏的邏輯沒有說明,需要考慮適當的增加一點注釋了。
其次也需要考慮注釋的質量:在代碼可讀性合格的基礎上,注釋應該提供比代碼更多的信息。文檔和注釋并不是越多越好,它們可能會導致維護成本增加。
## 如何編寫可發布的代碼
剛開始接觸高并發線上系統的新同學經常容易出現一個問題:寫的代碼在發布之后才發現很多考慮不到的地方,比如說測試的時候沒問題,項目發布之后發現有很多意料之外的狀況;或者出了問題之后不知道從哪下手排查,等等。
這里介紹一下從代碼編寫完成到發布前需要注意的一些地方。
### 處理異常
新手程序員普遍沒有處理異常的意識,但代碼的實際運行環境中充滿了異常:服務器會死機,網絡會超時,用戶會胡亂操作,不懷好意的人會惡意攻擊你的系統。
對一段代碼異常處理能力的第一印象應該來自于單元測試的覆蓋率。大部分異常難以在開發或者測試環境里復現,依靠測試團隊也很難在集成測試環境中模擬所有的異常情況。
而單元測試可以比較簡單的模擬各種異常情況,如果一個模塊的單元測試覆蓋率連50%都不到,很難想象這些代碼考慮了異常情況下的處理,即使考慮了,這些異常處理的分支都沒有被驗證過,怎么指望實際運行環境中出現問題時表現良好呢?
### 處理并發
而是否高質量的實現并發編程的關鍵并不是是否應用了某種同步策略,而是看代碼中是否保護了共享資源:
* 局部變量之外的內存訪問都有并發風險(比如訪問對象的屬性,訪問靜態變量等)
* 訪問共享資源也會有并發風險(比如緩存、數據庫等)。
* 被調用方如果不是聲明為線程安全的,那么很有可能存在并發問題(比如java的hashmap)。
* 所有依賴時序的操作,即使每一步操作都是線程安全的,還是存在并發問題(比如先刪除一條記錄,然后把記錄數減一)。
前三種情況能夠比較簡單的通過代碼本身分辨出來,只要簡單培養一下自己對于共享資源調用的敏感度就可以了。
但是對于最后一種情況,往往很難簡單的通過看代碼的方式看出來,甚至出現并發問題的兩處調用并不是在同一個程序里(比如兩個系統同時讀寫一個數據庫,或者并發的調用了一個程序的不同模塊等)。但是,只要是代碼里出現了不加鎖的,訪問共享資源的“先做A,再做B”之類的邏輯,可能就需要提高警惕了。
### 優化性能
性能是評價程序員能力的一個重要指標,但要評價程序的性能往往要借助于一些性能測試工具,或者在實際環境中執行才能有結果。
如果僅從代碼的角度考慮,有兩個評價執行效率的辦法:
* 算法的時間復雜度,時間復雜度高的程序運行效率必然會低。
* 單步操作耗時,單步耗時高的操作盡量少做,比如訪問數據庫,訪問io等,這里需要建立對各種操作的耗時的概念。
而實際工作中,也會見到一些同學過于熱衷優化效率,相對的會帶來程序易讀性的降低、復雜度提高、或者增加工期等等。所以在優化之前最好多想想這段程序的瓶頸在哪里,為什么會有這個瓶頸,以及優化帶來的收益。
再強調一下,無論是優化不足還是優化過度,判斷性能指標最好的辦法是用數據說話,而不是單純看代碼。
### 日志
日志代表了程序在出現問題時排查的難易程度,對于日志的評價標準有三個:
* 日志是否足夠,所有異常、外部調用都需要有日志,而一條調用鏈路上的入口、出口和路徑關鍵點上也需要有日志。
* 日志的表達是否清晰,包括是否能讀懂,風格是否統一等。這個的評價標準跟代碼的可讀性一樣,不重復了。
* 日志是否包含了足夠的信息,這里包括了調用的上下文、外部的返回值,用于查詢的關鍵字等,便于分析信息。
對于線上系統來說,一般可以通過調整日志級別來控制日志的數量,所以打印日志的代碼只要不對閱讀造成障礙,基本上都是可以接受的。
## 如何編寫可維護的代碼
可維護性要對應的是未來的情況,但是一般新人很難想象現在的一些做法會對未來造成什么影響。
### 避免重復
代碼重復分為兩種:模塊內重復和模塊間重復。兩種重復都在一定程度上說明了編碼的水平有問題。現代的IDE都提供了檢查重復代碼的工具,只需點幾下鼠標就可以判斷了。
除了代碼重復之外,還會出現另一類重復:信息重復。
比方說每行代碼前面都寫一句注釋,一段時間之后維護的同學忘了更新注釋,導致注釋的內容和代碼本身變得不一致了。此時過多的注釋反而成了雞肋,看之無用,刪之可惜。
隨著項目的演進,無用的信息會越積越多,最終甚至讓人無法分辨哪些信息是有效的,哪些是無效的。
如果在項目中發現好幾個東西都在做同一件事情,比如通過注釋描述代碼在做什么,或者依靠注釋替代版本管理的功能,這些都是需要避免的。
### 劃分模塊
模塊內高內聚與模塊間低耦合是大部分設計遵循的標準,通過合理的模塊劃分能夠把復雜的功能拆分為更易于維護的更小的功能點。
一般來說可以從代碼長度上初步評價一個模塊劃分的是否合理,一個類的長度大于2000行,或者一個函數的長度大于兩屏幕都是比較危險的信號。
另一個能夠體現模塊劃分水平的地方是依賴。如果一個模塊依賴特別多,甚至出現了循環依賴,那么也可以反映出作者對模塊的規劃比較差,今后在維護這個工程的時候很有可能出現牽一發而動全身的情況。
有不少工具能提供依賴分析,比如IDEA中提供的Dependencies Analysis功能,學會這些工具的使用對于評價代碼質量會有很大的幫助。
值得一提的是,絕大部分情況下,不恰當的模塊劃分也會伴隨著極低的單元測試覆蓋率:復雜模塊的單元測試非常難寫的,甚至是不可能完成的任務。
### 簡潔與抽象
只要提到代碼質量,必然會提到簡潔、優雅之類的形容詞。簡潔這個詞實際涵蓋了很多東西,代碼避免重復是簡潔、設計足夠抽象是簡潔,一切對于提高可維護性的嘗試實際都是在試圖做減法。
編程經驗不足的程序員往往不能意識到簡潔的重要性,樂于搗鼓一些復雜的玩意并樂此不疲。但復雜是代碼可維護性的天敵,也是程序員能力的一道門檻。
跨過門檻的程序員應該有能力控制逐漸增長的復雜度,總結和抽象出事物的本質,并體現到自己設計和編碼中。一個程序的生命周期也是在由簡入繁到化繁為簡中不斷迭代的過程。
簡潔與抽象更像是一種思維方式,除了要理解、還需要練習。多看、多想、多交流,很多時候可以簡化的東西會大大超出原先的預計。
## 如何做出優雅的設計
當程序的功能越來越多時,編程就不再只是寫代碼,而會涉及到模塊的劃分、和模塊之間的交互等內容。對于新同學來說,一開始很難寫出優雅的設計。
這一節會討論一下如何能讓自己編寫的代碼有更強的“設計感”。
### 參考設計模式
最容易快速上手的提升自己代碼設計水平的方式就是參考其他人的設計,這些前人總結的面對常見場景時如何進行模塊劃分和交互的方式被稱作設計模式。
### 設計模式的分類
* 創建型模式主要用于創建對象。
* 結構型模式主要用于處理類或對象的組合。
* 行為型模式主要用于描述對類或對象怎樣交互和怎樣分配職責。
由于篇幅有限,這里不再展開每一種設計模式的用途。這部分資料和書籍已經比較全了,可以課下學習。
### 編寫單元測試
### 單元測試是什么
維基百科上的詞條說明:
> 單元測試是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對于面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
所以,我眼中的“合格的”單元測試需要滿足幾個條件:
1. 測試的是一個代碼單元內部的邏輯,而不是各模塊之間的交互。
2. 無依賴,不需要實際運行環境就可以測試代碼。
3. 運行效率高,可以隨時執行。
### 單元測試的目的
了解了單元測試是什么之后,第二個問題就是:單元測試是用來做什么的?
很多人第一反應是“看看程序有沒有問題”。但是,只是使用單元測試來“看看程序有沒有問題”的話,效率反而不如把程序運行起來直接查看結果。原因有兩個:
1. 單元測試要寫額外的代碼,而不寫單元測試,直接運行程序也可以測試程序有沒有問題。
2. 即使通過了單元測試,程序在實際運行的時候仍然有可能出問題。
在這里我總結了一下幾個比較常見的單元測試的幾個典型場景:
1. 開發前寫單元測試,通過測試描述需求,由測試驅動開發。
2. 在開發過程中及時得到反饋,提前發現問題。
3. 應用于自動化構建或持續集成流程,對每次代碼修改做回歸測試。
4. 作為重構的基礎,驗證重構是否可靠。
還有最重要的一點:編寫單元測試的難度和代碼設計的好壞息息相關,單元測試測的三分是代碼,七分是設計,能寫出單元測試的代碼基本上可以說明這段代碼的設計是比較合理的。能寫出和寫不出單元測試之間體現了編程能力上的巨大的鴻溝,無論是什么樣的程序員,堅持編寫一段時間的單元測試之后,都會明顯感受到代碼設計能力的巨大提升。
### 如何編寫單元測試
測試代碼不像普通的應用程序一樣有著很明確的作為“值”的輸入和輸出。舉個例子,假如一個普通的函數要做下面這件事情:
* 接收一個user對象作為參數
* 調用dao層的update方法更新用戶屬性
* 返回true/false結果
那么,只需要在函數中聲明一個參數、做一次調用、返回一個布爾值就可以了。但如果要對這個函數做一個“純粹的”單元測試,那么它的輸入和輸出會有很多情況,比如其中一個測試是這樣:
* 假設調用dao層的update方法會返回true。
* 程序去調用service層的update方法。
* 驗證一下service是不是也返回了true。
而具體的測試內容可以依賴單元測試框架提供的功能來完成。
### 單元測試框架
運行框架:
* jUnit
* TestNG
* Spock
Mock框架:
* Mockito
* EasyMock
* PowerMock
* Spock
由于篇幅限制,這里不再展開具體的框架用法了,有興趣的同學可以自行搜索相關文章。
## 如何規劃合理的架構
很多新同學的規劃都是未來成為架構師,要做架構師就免不了設計架構。而在微博平臺工作也會經常跟架構打交道,由于后面有獨立的架構課程,這里只是簡單介紹一下常見的架構模式。
### 常見的架構模式
### 分層架構
分層架構是應用很普遍架構模式,它能降低模塊之間的耦合,便于測試開發,它也是程序員需要掌握的基礎。
典型的分層架構模式如下:

上圖中是一個4層的架構,在現實場景中,層數不是一個定值,而是需要根據業務場景的復雜度決定。使用分層模型中需要注意,一般來說不能跨層、同層或反向調用,否則會讓整個層次模型由單一的樹形結構變為網狀結構,失去了分層的意義。
但隨著程序復雜度的逐漸提升,要嚴格的按照分層模型逐級調用的話,會產生很多無用的空層,它們的作用只是傳遞請求,這也違背了軟件設計盡量簡潔的方向。所以實際場景中可以對各個層次規定“開放”或“關閉”屬性,對于“開放”的層次,上層可以越過這層,直接訪問下層。
對層次定義“開放”或“關閉”可以幫助程序員更好的理解各層次之間的交互,這類約定需要記錄在文檔中,并且確保團隊中的每個人都了解這些約定,否則會得到一個復雜的、難以維護的工程。
### 事件驅動架構
事件驅動架構能比較好的解耦請求方和處理方,經常被用在寫入請求量變化較大,或者是請求方不關心處理邏輯的場景中,它有兩種主要的實現方式:
### Mediator
在mediator方式中,存在一個中介者角色,它接收寫入方的請求,并把事件分配到對應的處理方(圖中的channel),每個處理方只需要關心自己的channel,而不需要與寫入方直接通信。

在微博前些年應用比較多的應用服務-隊列-消息處理服務可以認為是屬于這種模式。
### Broker
在broker方式中不存在中介者的角色,取而代之的是消息流在輕量的processor中流轉,形成一個消息處理的鏈路,如圖:

前一段時間開始推廣的storm流式處理屬于這種模式,對于較長的處理流程,用broker方式可以避免實現Mediator的復雜性,相對的,管理整個流程變得復雜了。
### 微內核架構(Microkernel)
微內核架構相對于普通架構最主要的區別是多了“內核”的概念,在編寫程序時把基礎功能和擴展功能分離:內核中不再實現具體功能,而是定義“擴展點”,增加功能時不再修改主邏輯,而是通過“擴展點”掛接到內核中,如圖:

之前介紹的motan RPC框架應用了這種設計,這讓motan可以通過不同的插件支持更多的功能,比如增加傳輸協議、定義新的服務發現規則等。
### 微服務架構
近年來微服務架構的概念一直比較火,它可以解決服務逐漸增長之后造成的難以測試及部署、資源浪費等問題,但也帶來了服務調度和服務發現層面的復雜度,如圖:

微博底層實際包含了很多業務邏輯,這些業務邏輯被抽象成一個個服務和模塊,不同模塊之間通過motan rpc、grpc或http rest api進行通信,通過docker和之上的調度服務進行調度和部署,最終成為一個完整的系統。
微服務隔離了各服務之間的耦合,能夠有效提升開發效率;除此之外,當微博面對巨大的流量峰值時,可以進行更精細的資源調配和更有效率的部署。
### 單元化架構
傳統的分層架構往往會存在一些中心節點,如數據庫、緩存等,這些節點往往容易成為性能瓶頸,并且存在擴容比較復雜的問題。
在面臨對擴展性和性能有極端要求的場景時,可以考慮使用單元化架構:對數據進行切分,并將每一部分數據及相關的邏輯部署在同一個節點中,如圖:

在單元化架構中,每個“單元”都可以獨立部署,單元中包括獨立的計算和存儲模塊;計算模塊只與單元內的存儲模塊交互,不再需要分庫分表等邏輯;而數據與存儲更近也降低了網絡消耗,進而提高了效率。
微博平臺通過對群發服務的單元化改造,實現了百萬級每秒的私信推送服務。
## 如何處理遺留代碼
對于一個不斷發展的系統,必然有一些遺留下來的歷史問題。當遇到了遺留下來的爛代碼時,除了理解和修改代碼,更重要的是如何讓代碼朝著好的方向發展,而不是放任不管。
重構是處理遺留代碼的比較重要的手段,這一節主要討論一下重構的相關話題。
### 何時重構
新同學往往對重構抱有恐懼感,認為重構應該找一個比較長的時間專門去做。這種愿望很好,但是現實中一般很難找出一段相對穩定的時間。
另一方面,重構是比較考驗編程水平的一項工作。代碼寫的不好的人,即使做了重構也只是把不好的代碼變了個形式。要達到比較高的水平往往需要不斷的練習,幾個月做一次重構很難得到鍛煉,重構效果也會打折。
所以,重構最好是能夠作為一項日常工作,在開發時對剛寫完的代碼做重構往往單位時間的收益是最大的。
### 如何重構
一般來說,重構可以抽象成四個方面:
#### 理解現狀
如果對當前程序的理解是錯的,那么重構之后的正確性也就無從談起。所以在重構之前需要理解待重構的代碼做了什么,這個過程中可以伴隨一些小的、基本無風險的重構,例如重命名變量、提取內部方法等,以幫助我們理解程序。
#### 理解目標
在理解了程序做了什么事情之后,第二個需要做的事情就是需要提前想好重構之后的代碼是什么樣的。
改變代碼結構比較復雜,并且往往伴隨著風險和不可控的問題。所以在實際動手之前,需要從更高的層次考慮重構之后的模塊如何劃分,交互是如何控制等等,在這個過程中實際與寫代碼要做的事情是一致的。
#### 劃分范圍
爛代碼往往模塊的劃分有一些問題,在重構時牽一發而動全身,改的越多問題越多,導致重構過程不可控。所以在動手重構前需要想辦法減少重構改動的范圍,一般來說可以只改動相鄰層次的幾個類,并且只改動一個功能相關的代碼,不求一次性全部改完。
為了能夠劃分范圍,在重構時需要采用一些方法解除掉依賴鏈。比如增加適配器等等,這些類可能只是臨時的,在完整的重構完成之后就可以刪除掉,看起來是增加了工作量,但是換來的是更可控的影響范圍。
#### 確保正確
為了能保證重構的正確性,需要一些測試來驗證重構是否安全。最有效的是單元測試,它能提供比集成測試更高的覆蓋率,也能驗證重構之后的代碼設計是否是合理的。
在做一次重構之前需要整理模塊的單元測試。遺留代碼有可能測試不全,并且難以編寫單元測試,此時可以適當的犧牲待重構代碼的優雅性,比如提高私有方法的可見性,滿足測試的需求。在重構的過程中,這部分代碼會被逐漸替換掉。
## 總結
今天跟大家討論了一下關于編程的各個方面,關于編程的話題看似很基礎,想要做好卻并不容易。新同學比較容易急于求成,往往過多的關注架構或者某些新技術,卻忽視了基本功的修煉,而在后續的工作過程中,基本功不扎實的人做事往往會事倍功半,難以有更一步的發展。
勿在浮沙筑高臺,與各位共勉。
**新兵訓練營簡介**
微博平臺新兵訓練營活動是微博平臺內部組織的針對新入職同學的團隊融入培訓課程,目標是團隊融入,包括人的融入,氛圍融入,技術融入。當前已經進行4期活動,很多學員迅速成長為平臺技術骨干。
具體課程包括《環境與工具》《分布式緩存介紹》《海量數據存儲基礎》《平臺RPC框架介紹》《平臺Web框架》《編寫優雅代碼》《一次服務上線》《Feed架構介紹》《unread架構介紹》
微博平臺是非常注重團隊成員融入與成長的團隊,在這里有人幫你融入,有人和你一起成長,也歡迎小伙伴們加入微博平臺,歡迎私信咨詢。
**講師簡介**
秦迪,[@蛋疼的AXB](http://weibo.com/n/%E8%9B%8B%E7%96%BC%E7%9A%84AXB)?微博平臺及大數據部技術專家
個人介紹:http://blog.2baxb.me/about-me