天天看點

如何提高代碼品質?

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

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

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

本文從 産品,接口,名額,日志,代碼清晰度,代碼複雜度 等方面,談談如何提高代碼品質。

産品和接口

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

産品經理的工作是什麼?是把複雜的邏輯用清晰的,易用的方式(接口)展現給使用者。

程式員的産品是代碼,代碼的使用者是其它程式員 —— 是以高品質的代碼是讓别的程式員容易了解,容易使用的代碼。注意,這個層次的容易了解,是指結構,原理和接口上容易了解,而并非代碼的細節容易了解。

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

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

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

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

如何提高代碼品質?

舉些具體的例子。

前端工程師是把使用者的 url 請求,parse 成浏覽器 DOM 上的一系列 component,把使用者的行為,parse 成某種内部的事件 {event_type, event_data},并且進一步由 event_type parse 成某個 event_handler —— 然後這個 handler 繼續 parse event_data,直到其轉化成新的 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 的感覺。

原文釋出時間為:2018-07-09

本文作者:陳小天

本文來自雲栖社群合作夥伴“

Golang語言社群

”,了解相關資訊可以關注“

繼續閱讀