【華爲雲技術分享】程序員真香定律:源碼即設計

我們經常談論架構,討論設計,卻甚少關注實現和代碼本身,架構和設計固然重要,但要說代碼本身不重要,我不同意,Robert C.Martin大叔也不同意,Martin認爲“源碼即設計”。

在討論具體的實施細則之前,我們不妨討論一下什麼是好代碼?蘿蔔特 C.Martin認爲:衡量代碼質量的唯一標準是:WTF/min,也就是review代碼的時候每分鐘說“握草”的次數。這個定義雖有辱斯文,但粗野中不失奔放,調皮中又蘊含哲理。

好的代碼如同文筆優美的散文,行雲流水,賞心悅目,閱讀的時候,如沐春風,帶給人愉悅與啓迪。

好的代碼猶如構思精巧的小說,它或許不夠平鋪直述,卻足夠引人入勝,讀到最後,你會豁然開朗,我去,原來是這樣的啊,那一刻,你會覺得過程中的曲折和探索都是值得的。

好的代碼,透過一個個函數,你彷彿可以窺視到作者有趣的靈魂;透過一行行代碼,你彷彿在與一個充滿智慧的朋友聊天,她總是條理清晰,邏輯嚴謹,有條不紊,娓娓道來。

而壞的代碼,猶如病毒,它不僅癱瘓你的程序,還有很強的傳播效應,等到它擴散開來,神仙難治。

壞的代碼,像一個泥團,閱讀的時候,你彷彿被困於黑暗的迷宮,又彷彿在跟一個絮絮叨叨的人交談,她的腦回路經常短路,說話含混不清,主次不分,叨逼半天,你依然get不到她的中心思想,你常常感覺智商受到了莫大的侮辱,你面露艱難神色,心中萬馬奔騰。

有很多區分好壞代碼的規則,我也看過一些,對於文章中提到的一些標準做法,就不重複嚼舌頭根子了,我想結合自己的工作經歷,談一談自己的切身體會。

閒扯半日,言歸正傳,要編寫瀰漫好味道的代碼,要遵循哪些約束和指引呢?

 

一致性

持之以恆的遵從一致性規則,在代碼風格上,爭論個三天三夜估計也定不出個好壞出來,但好的風格一定是強一致性的,這一點應該比較容易達成一致吧?風格的好壞其實更多受習慣的影響,頭髮少一點的程序員應該都有自己風格變遷的經歷,多年前自己篤信不疑的good style或許正是當前深惡痛絕的bad style,所以我主張在style上擱置嘴炮,一個項目應該有一個編碼規則,好的規則應該是以理服人的,好的規則應該是拒絕任性夾帶私貨的,規則定了之後,就遵照執行吧,可能某個風格跟你不相符,但沒關係,你要知道,這並不意味,你在style之戰敗下陣來,也並不表示它說服了你,你遵守的是規則和紀律本身。

變量(包括文件、類/結構體、函數)命名,比如ohmygod,你可能搞不清哪些字母是一夥的,所以需要界定單詞。駝峯通過單詞首字母大寫來界定單詞,另一個慣用做法是用下劃線拼接單詞。駝峯的弊端是醜,下劃線拼接的弊端是增加了標識符長度(相比首字母大寫),好處是跟std c/c++、linux kernel的做法一致,喜歡kernel的碼農容易找到如家般的歸屬感。

c++有namespace避免衝突,c經常用prefix防止命名污染全局空間,但我認爲命名簡潔扼要很重要,所以我支持簡短的前綴而反對冗長的前綴。

 

代碼密度

實現同樣的功能,你喜歡100行代碼,還是20行代碼?如果貴leader不以代碼行數考覈績效我建議你把代碼寫的精簡,而如果貴leader以代碼行數考覈績效,我建議你轉行,開滴滴,送外賣或者擺攤都行,因爲在這樣的leader下面耗費青春基本上也不會有什麼發展前途。

把簡單的東西搞複雜化很容易,你只需要找一個能力平庸的人就能實現化簡爲繁的願望,而化繁爲簡則堪稱化腐朽爲神奇。也許你要說,我欠缺簡化的能力,這並不奇怪,坦白講,這不是一件容易的事,你做不到沒關係,但你擁有正確的理念更重要,它將幫助你認清前進的方向,而不是在錯誤的道路上越走越遠。

有些項目,充斥各種無效代碼,其實你只需要稍加思考,你就能識別出來。

比如大塊註釋掉的代碼像發臭的屍體一樣遍佈其中。比如大量功能重複的代碼像垃圾一樣堆砌在那裏。比如本不需要返回值的函數恆定的返回true。

又或者函數一進來,不管三七二十一,對入參一頓檢查,全然忘記你在編寫的是一個私有實現函數,你在調用它之前已經檢查過一遍,私有函數是一個受控的安全上下文,這不僅不優雅而且不綠色(低效耗電)並且不安全(在該崩的時候沒崩把雷埋到了更隱蔽的地方),話說你看標準庫函數strcpy/strcat,vector operator[]檢查傳參了嗎?

提高代碼密度或者說濃度有利於理清思路,有利於突出重點,有利於提高維護性,而充斥各種無效語句的代碼只會把關鍵語句淹沒在汪洋大海,使得review代碼的人get不到重點,看不清主次。像聽一個絮絮叨叨的人做報告,滿篇廢話,像看一個劇情拖沓的連續劇,昏昏欲睡,像喝一瓶二鍋頭兌十斤白開水,口能淡出個鳥來。

重構是程序員的口頭禪,重構是在保持程序功能不變的情況下調整架構和實現,我認爲提高代碼密度應作爲重構的一項追求。

linux kernel、lua、nginx、skynet這些優秀的開源庫代碼濃度都很高,建議讀者朋友品嚐一下。

 

封裝

我們最常乾的一件事就是把重複編寫的代碼封裝到一個函數裏去,用多處調用替代重複編寫,這個很好理解,但其實即使不被多處調用,把相關的一段代碼封裝到一個實現函數也是有必要的,因爲把代碼平鋪開來,把細節暴露出來,容易掩蓋重要的東西,即框架和脈絡會變得不夠清晰。

一個見名知義的函數調用比堆砌在那裏的一段代碼給我的感受好,我如果關心它是怎麼做的,我可以跳轉到定義看看實現。

封裝的一個核心原則是單一職責,符合單一職責的函數更易於被複用。

 

避免特例

linus大神分享過他心中的好代碼,說的是針對鏈表的操作,他更喜歡統一性的處理方式,而不是做特例化的處理,我想這個例子很有代表性,它其實代表一種理念,那就是自始至終,我們的頭腦裏必須優先考慮normal化的處理方式,當然這其實是一個比較高層次的要求,菜鳥互啄可以先跳過這一層要求。

 

縮寫

慎用縮寫,相比縮寫帶來的含混不清,我寧願多敲幾下鍵盤,如果要縮寫請符合慣例遵從常規,比如AI,比如App,比如cfg,但是你把threshold縮寫成threshod,把Item縮寫成Iem,我特木真的搞不懂這是拼錯了還是縮歪了?

 

解耦

構建鬆散耦合的系統一直是軟件工程的一個目標,模塊化的一個方向便是解耦,但我們口口聲稱心心念想的解耦,在實施層面又有幾分體現呢?

比如,我經常乾的一件事就是把類似配置文件,或者宏定義的東西集中的一個頭文件裏去,看起來很統一也很正規,起碼我之前也是這樣認爲的,但忽然有一天,發現自己這樣做顯得很不聰明的樣子,爲什麼呢?因爲你想把所有模塊配置相關的東西都塞進配置公共文件真的合適嗎?是不是把公共接口抽離出來更好,把配置相關的數據下沉到各模塊更合適?

另外,把宏都定義到一起,這意味隨便改點東西,都會需要修改宏頭文件,而這個頭文件就會成爲程序世界的中心,修改公共宏文件幾乎會引起整個系統的所有源文件rebuild,這簡直就是AOE團滅啊。所以更好的方式是分而治之,去集中式。

我們知道c/c++的編譯單元是source file(.c/.cpp),編譯的第一步是預處理,所有include都會展開替換,所以我們要避免引入任何不必要的頭文件,也應該把本編譯單元用到的頭文件都include進來,這就是所謂的頭文件自給自足。這點很重要,但很多人會不以爲然,甚至有些人會自作聰明的搞一個allincluded.h,把常用的一些頭文件全部include進來,然後自認爲一勞永逸的完美的解決了問題,包含不必要的頭文件會增加編譯時間,會增加依賴,我們不僅應該避免錯誤的包含,還應該精心設計和劃分文件,使得每個文件的功能足夠內聚單一。

 

遵從標準

我遇到每個模塊單獨定義自己的各種原生(build-in)數據類型,但我建議不要這樣做。如果你只是需要解決不同體系結構下long等整型的長度差異,我想告訴你,c庫頭文件stdint.h已經從標準層面統一解決了這個問題,裏面int8_t/16_t/32_t/64_t,還有uint8_t等等應有盡有。

 

宏是c的一個有效武器,在有些情況下確實行之有效,關於宏,我是騎牆派,我既反對禁用宏,也反對濫用宏,inline可以部分替代宏,但不能完全替代宏。

如果項目裏到處都是宏,全大寫,至少1/3的代碼都是各種詭異的宏,你review代碼的時候,不停的跳來跳去,看了一眼,哦,就這樣啊,然後切回來,頻繁的上下文切換是低效的,它打斷了你的思路,其實很多時候完全沒有必要。

 

命名

命名有一些指引,比如類/結構體應該用名詞,函數應該用類似動詞或者doSomething這樣的動賓結構,這些規矩都是耳熟能詳的。

我主張命名應該簡明扼要,不要羅裏吧嗦,要準確的表達出它要做的事情,如果你碰到命名困難,你可能需要考慮你的類定義或者接口劃分是否合適。

命名是接口的一部分,很重要,好的命名是自注釋的。

我反對匈牙利命名法,理由:不能一致性的解釋各種類型,把類型編碼進變量不合理,變量名本身就能體現它的類型,無法適用template情況,始作俑者ms放棄了它。

如果你沒有思路,那我建議你參考一下STD C/C++ API,畢竟這些接口歷經幾十年沒有大的變化,算是經受住了歷史的考驗,比如malloc/free/atoi,stl 容器的成員函數也有點意思:size()、 capacity()、resize()、reserve()、push()、pop()、top()、back(),很乾脆,不廢話,我覺得很好。

所以,如果你編寫的是某某管理器,比如ItemManager,我建議你直接取名add(),remove(),而不用AddItem(),RemoveItem(),因爲你本身就是Item的Manager,操作的必然是Item,而且從參數上也能體現出來,少即是多,多不如少。

 

擴展性

開閉原則是應對擴展性的rule,人無遠慮必有近憂,說的是我們不能侷限於眼前,但也請不要過度設計,不可盲目迷信擴展性,戲太多也是病。

知乎有一篇神貼講的是如何把helloworld搞成一個big project,當你想給別人項目挑刺的時候,你可以用擴展性說事,但我建議你離開口閉口擴展性的人遠一點,據我觀察,這種人大多比較虛僞而且很水。

 

高效而魯棒

有很多避免運行低效的做法,比如哈希、減少拷貝、提高局部性、buffer/cache、空間時間置換、內聯、分支預測、判斷前置、計算延遲、無鎖編程。

提高魯棒性的關鍵是保持簡單,任何引入複雜性的動作都需要保持足夠警惕。不信任/零信任設計,面向failed編程,假設依賴的上下文,上下游都是不可靠的,去中心化去關鍵路徑,熔斷,降級,避免驚羣效應,方法很多,不一一列舉了。

 

作者:華爲雲專家  人民副首席碼仔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章