當前位置:
首頁 > 最新 > 如何提高代碼質量?

如何提高代碼質量?

好的程序員從來不靠格子衫或者顏值吃飯,就像你家 C 羅明明可以靠臉,卻非要用不斷精進的身體和技術迷倒你。

對偉大前鋒來說,進球,以及一個能夠迸發出進球能力的身體非常重要。

對靠譜程序員來說,代碼質量,以及一顆能夠洞悉高質量軟體編寫之道的大腦彌足珍貴。

本文從產品介面指標日誌代碼清晰度代碼複雜度等方面,談談如何提高代碼質量。

產品和介面

好的產品經理未必是個好的程序員,但好的程序員一定是個好的產品經理。

產品經理的工作是什麼?是把複雜的邏輯用清晰的,易用的方式(介面)展現給用戶。

程序員的產品是代碼,代碼的用戶是其它程序員 —— 所以高質量的代碼是讓別的程序員容易理解,容易使用的代碼。注意,這個層次的容易理解,是指結構,原理和介面上容易理解,而並非代碼的細節容易理解。

細節在產品這個層次,一定要隱藏起來。用戶在打開瀏覽器,訪問 arcblock.io 的時候,並不需要關心 DNS 是怎麼工作的,PKI 體系是怎麼運作的,HTTP / TLS / TCP / IP 協議是什麼,報文是怎麼從 user space 交付到 kernel space,再怎麼 DMA 到網口發送出去 —— 這還沒完,接下來出場的,還有負責 l2 protocol 的 switch,保護你安全的 firewall,郵遞員 router,以及明明概念上是網路技術,卻整個青春都錯付給了安全的 NAT。。。

如果產品經理做的產品展示給用戶是這樣巴拉巴拉的細節,那麼丫一定會被扯爛暫住證,大耳光從天黑抽到天亮,然後早班綠皮車送到清河去挖沙;如果程序員的 如此啰嗦,不管人家受得了受不了,那麼他這輩子篤定找不到同性朋友,更別說異性了。

所以程序員在寫代碼之前,先要想想如果這是一篇演講稿,我該如何說起?我能在三五分鐘講清楚這代碼要幹什麼?有沒有生活中或者同行會心一笑立刻 get 到的例子可以類比?

90% 以上的情況,程序員是在寫 parser。換句話說,我們寫的絕大部分代碼就是把一系列的輸入,經過若干轉換(transformation),變成一系列輸出。

舉些具體的例子。

前端工程師是把用戶的 url 請求,parse 成瀏覽器 DOM 上的一系列 component,把用戶的行為,parse 成某種內部的事件 ,並且進一步由 parse 成某個 —— 然後這個 handler 繼續 parse ,直到其轉化成新的 DOM,或者對後端的某個 API 的某個請求。

對於 API 來說,它 parse request,生成 response。request 可能被 parse 成一個 sql,交付給 database;也可能被 parse 成滿足另一個服務介面的 request(比如 grpc),交給另一個服務。這樣周而復始,直到 API 收集完七顆龍珠召喚神龍各個服務的所有數據,再 parse 成一個合規的 response,交還給 client。

所以程序員看待自己的代碼產品,要像庖丁看待肥牛一樣 ——「未見全牛」,「神遇而不以目視」,「以無厚入有間」—— 滿眼望去,就是一個個 parser,大的 parser 掛小的 parser,再掛更小的 parser。每層,甚至每個 parser,都是個 pipeline —— 它們一般由 validator,serializer,transformer 等介面組織起來,輔以各種 builder,decorator,factory,commander,再加上為之而生的 tools,utility,helper 等搭建而成。

這樣一層層組織下來,該粗的地方粗,該細的地方細,遇人說人話,遇猿說猿語,代碼可伶可俐,可蘿可御。

接下來,是很重要卻最讓人撓頭的事情,給你的大大小小的模塊取名。名字傾注著感情,就像寒夜裡小女孩划下的火柴,酣戰一宿的聖盔谷外甘道夫揮起的魔杖,給人以光明,溫暖,希望,以及讀到時觸電般的「我懂你」。

肖申克的救贖里有段,午餐時 Andy 問大夥那個前夜裡被打死的可憐的胖子叫什麼?大夥一臉懵逼,說我 TM 為什麼要關心一個死胖子的名字。這一幕看著很痛,就像華安在成為華安之前,只有一個如螻蟻般微渺的代號。如果你想讓你的代碼不是一個讓人漠視的死胖子,而是人們願意談論,那麼,取個容易讓人理解,甚至讓人刻骨銘心的名字吧。

不好的名字除了讓人不解,漠視,甚至宛如與人世間幽隔的惡鬼,望上一眼,大家便想逃離;好的名字,嗯,隨便說一個,聶小倩,同樣是與人世間幽隔的孤鬼,你我卻念念不忘。

在 Juniper,我最忘不了的兩台伺服器是 gretel 和 hansel,取自格林童話;在途客圈,讓我心心念之的項目是 atlantis 和以及其上 viking (code name) —— 這不難理解,要追尋 atlantis,你需要遠征 (viking);在 tubi,cms service 是個糟糕的取名,merlin 算是回歸了正途,雖然作為一個 build service,它的魔力並不太強,還時不時失靈;而在 arcblock,我在上篇文章里談到的 AADL,被正式取名 AODL —— 這不重要,估計你也記不住,不過,她有了一個對外的名字:goldorin —— 托爾金為中土大陸精靈族發明的精靈語。

在代碼命名:僧敲月下門那篇文章里,我提到晦澀的 IKE 代碼里 pitcher / catcher 讓協議的 negotiation 讀來猶如欣賞棒球比賽。好的名字,和好的介面幾乎成對出現,它讓程序員的產品 —— 代碼,變得鮮活,讀來如沐春風,如飲醇酒,如賞佳人。

指標和日誌

好的產品是在改進中不斷提升的,就像鳳凰,經歷烈火不斷煎熬,得以涅槃。而要想改進,離不來測量 (measure),它是構建 (build) 和學習改進 (learn) 中間最重要的一環。

熱力學第二定律是最讓人討厭也最讓人無奈的定律。它直接導致了「不運動肚子上的贅肉必然增加」,「不收拾房子房子會越來越亂」,「不持續改進代碼,代碼的質量會越來越低」這些讓人煩心的事情。

而這個破定律的祖師爺 Lord Kelvin 說:

嗯,測量很重要,非常重要。如果構建和改進是兩根枝杈,測量就像蜘蛛在兩者間掛下的網,這網越密,兩根樹枝間的路就越多,就越容易從一端走到另一端,循環往複。

對於測量的途徑主要是指標 - metrics 和日誌 - logs。metrics 像是心電圖或者 CT,讓身體的狀況一覽無餘。所以 metrics 用來了解現狀,指明方向;logs 則是細密的日記,什麼都有,唯獨沒重點,所以常常在現狀和問題的方向確定後,用來歸因。比如說 CT 報告說,這周和上周相比,肝不那麼好了,需要小心肝。那麼肝為什麼不好?把一周的日誌調出來一看,哎呀,夜夜酒吧里縱情於世界杯,難怪。於是得出改進方案:世界盃結束後,別又喝酒又熬夜又賭球這病就好了,沒事。

metrics 和 logs 大部分時候是給自己和別的程序員看的,所以從上文的角度看,它也是個產品,符合產品和介面定義的一切準則。

先說 metrics。

定義 metrics 的時候,你要先搞明白你要改進些什麼,這是所謂的 begin with the end in mind。代碼的運行效率?那麼,究竟那裡效率不高?怎麼定義效率,怎麼計算效率(latency? throughput? 還是什麼)。代碼的容錯性?那麼,什麼樣的 error 要收集,如何分門別類?哪裡是潛在的錯誤大本營?

知道要改進什麼後,接下來腦袋裡要有幅圖 —— 不是富春山居圖 —— 是自己或者別人使用這些 metrics 的場景預現圖,就像至尊寶給山賊展示他和白晶晶的曠古奇戀的畫面一樣。

比如說要提高效率,並且確定是降低 latency,所以打算收集服務的 response time,那麼,response time 是看 line chart 還是 bar chart,知道了 latency 突然升高這件事之後,下一步呢?怎麼知道再看什麼?要和其它 metrics / event 關聯么?關聯哪些,怎麼關聯?想想意外事件發生之後,作為唯一可以背鍋的程序員,身後一堆產品運營盯著你的屏幕,喪著個個臉,表情比出殯還悲壯,好像你一秒鐘給公司損失幾十萬上下似的。在緊張的汗水打濕了你的格子衫時,你能看些什麼,你該看些什麼?

這樣從解決什麼問題,收集什麼 metrics,怎麼關聯使用 metrics,一層層定義下來之後,我們可以確保兩件事情:1. 當壞事發生的時候,我第一個知道。比如:對外的 API 的 95 percentile 的 response time 過去 5 分鐘突然增加了 30%。2. 我能快速鎖定問題的大致範圍。比如:從其它 metrics 上看,是因為 diagon alley 服務的 latency 突然升高,進一步地,diagon alley 的 disk write IOPS 顯著提高。那麼這個問題,我就看為什麼 diagon alley 的 disk write 不正常。

接下來是 logs。

logs 是不出問題不必太在意,但一旦出問題一定要能夠方便定位具體的位置的奇葩重要 數據。所以 logs 求充足具體,要像辭海一樣廣而全 —— 比如當 metrics 告訴我們,問題出在我們並不清楚茴香豆的「茴」字時有幾種寫法,logs 能夠幫助我們快速翻出來有用的那段,然後找出「茴」的四種寫法。

logs 兼具給人看,和給機器分析兩種效用,因而,最好要固定格式,以方便機器分析;但又不要用類似 JSON 的供機器閱讀的方式,如果不配合一個好用的 parser,當人閱讀時像是韓式整容過的足球寶貝,或者被抽幹了形容詞的句子,每個都長得一個模樣,需要摘了眼鏡用放大鏡仔細找不同。

通過合理的 metrics 和 logs,測量變得唾手可得。這便釋放出來我們不斷迭代不斷改進的能力。同樣起點的代碼,同樣水準的程序員,一個一周迭代一次,一個一天迭代一次,其累進的質量在若干周期之後,會有質的變化。

代碼清晰度和代碼複雜度

如果上面幾個方面都做好了,代碼的質量再差也是有下限的。這個下限可以通過嚴格使用 linter 和不斷提升對所用語言的掌握來提高。就好比一個會獨立思考並勤于思考的人,他的文章值得一讀,也許從遣詞造句,從修辭手法,從原起承提來說,他還稚嫩,但那是下限,並且是很容易提升的下限。

在 elixir 的 linter 里,我把 ABC complexity size 設置為 70,Cyclomatic complexity 設置為 15。所謂 ABC complexity,是代碼里的 assignments(A),branches(B),conditionals(C) 的平方之和開方根的結果,它代表了一段代碼有多冗長。Cyclomatic complexity,或者說循環複雜度,是指由程序的源代碼中量測線性獨立路徑的個數,它代表了一段代碼有多難懂(我們的小腦仁最不擅長同時記幾件事情,比如情人節和結婚紀念日)。還有一些其他的設置,比如 nesting(嵌套層數)不超過 3, arity(函數的秩,或者說參數個數)不超過 6 個等等。這些 lint 的約束,會強迫你在函數的實現細節層面,考慮地更好。大部分情況下,同一個功能的代碼可以有不同的表述方式,linter 的目的就是建立約束,強迫你用更合理的方式去表達一個功能點。

比如我常常不經意寫出的代碼:

這樣降低了代碼的 complexity,提高了代碼的 clarity,同時,還使得代碼的 extensibility 大大提升 —— 以後要加一個 「type 3」 的處理,僅僅是加一個簡單的函數而已,非常符合 open/close 原則。

這樣的小技巧有賴於對語言的精進,和對 linter 規則的恪守。雖然例外偶有發生 —— 比如一個複雜的 sql query 用 Ecto 表述很容易超過 ABC,但絕大多數情況,守著規則,會讓你受益 —— 每次 commit,過 linter 就像靈魂在桑拿房裡給蒸氣熏碾,痛苦難耐。勉力熬過去後,推門出去一下子無比清爽,有種撥雲見日,level up 的感覺。

禪定時刻

為革命保護視力,預防近視,眼保健操,開始!

眼睛是心靈的窗戶,更是程序員除了左手和右手之外,攻城略地,氣吞山河的利刃。最近重溫了曾經折磨陪伴 自己十二年的眼保健操曲子,豁然發現,原來我們保護視力,是為了革命啊!你看,還好我小時候認真做眼保健操,否則要錯過多少革命:通訊革命,互聯網革命,移動互聯網革命,雲計算革命,大數據革命,庵の,區塊鏈革命。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序人生 的精彩文章:

TAG:程序人生 |