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