「源碼風格」究竟能解決什麼問題?
![](https://assets.matters.news/embed/a6ca7756-3863-464c-b6a8-bb29c21186e8.png)
軟體開發的復古話題
同事發起了一個討論議題:是否需要有一個統一的「源碼風格(code style)」,讓大家更好的解決問題或改善流程?
雖然這不是軟體開發中月經文等級的討論議題,但真的也是一個從以前就〝熱烈討論〞到現在的議題(應該僅次於「xxx是最好的程式語言」這種…)。很多人都覺得源碼風格統一好讚好重要,好的風格可以帶你上天堂,壞的風格可以讓你關戰鬥房。好像只要源碼風格統一了,就可以讓源碼覆審(code review)變得更高效,或是可以讓大家更快看懂大神的作品,馬上可以「與神同行」,總而言之就是源碼的品質會變得更好就是了。我自己的經驗是沒有這些療效啦,但我也不是真的看過多少人的程式,所以只能說說個人觀感,不敢說給大家有什麼指導,大家參考看看吧。
就美觀整潔而言,必然是會有一些〝調節心情〞的效果。畢竟,去看別人的源碼實在是一件苦差事,有一定的困難度,若是風格統一,整理的漂亮整潔,至少WTF的分數不會那麼高…
問了問身邊的同事:「把你的Java Code改成用C的源碼風格呈現的話,你會因此就難以理解同一份程式嗎?」,或是反過來:「若同事的源碼和你的Domain完全不同,但和你的源碼風格一致,你會因此覺得這份源碼比較容易理解嗎?」
我真的都得到肯定的答案,雖然有點難以理解。
我想合理的解釋,或許是一種「熟悉感」吧?大概就像是工程師的「安全感」一樣的。我自己是認為〝對理解有幫助〞是完全沒道理的,比較合理的解釋應是〝去理解的「意願」有幫助〞,這樣就說得過去了。
在真正進入「解決問題」或是「改善流程」的主題前,我們得先定義什麼叫做「源碼風格」。
「風格」就是指同一件事的不同表述法
網路上許多「源碼風格」的文章都討論的轟轟烈烈的,但這些文章都沒有針對「風格」的範圍做嚴謹的定義,只是大略說一下包括空白/跳格(space/tab)的使用及個數啦,大括號怎麼放啦,變數/函式怎麼命名啦,甚至是一些for迴圈的寫法都變成是風格的一部分了。也就是因為這樣,所以討論起來常常就變得很發散,這個問題始終都得不到一個良好的共識,大部分的文章最後就是說風格沒有誰好誰壞啦,大家統一就好什麼的。
若我早個10年寫這篇文,我肯定會想好一陣子無法定案,但現在再來看同一件事,就好像沒那麼困難了。現在許多遊戲都有「貼圖」或是「特效」這種東西給你買,例如SF5的Ken及Ed,你把他們的貼圖換成DMC的Dante和Nero,他們不會因此把劍或惡魔之手拿出來用,使用的還是升龍拳及Psyco Blow這些招式。特效也是一樣,FPS中的爆頭本來可能都是噴血,你買了個特效改噴彩帶,不會讓你爆頭的準確率有所提升。在GamePlay不變的前提下,讓遊戲的表現看來更有自己想要的樣子,這就是風格。
好,所以「風格」的定義很清楚了,在產品運作邏輯沒有改變,效能沒有重大影響的前提下,小至括號放哪裡,大至類別關係怎麼定義都是「風格」中的一環。源碼就算長的像整形失敗的佛萊迪,只要邏輯是對的,產品就是對的。但不同的是,源碼的長相不好,確實會影響到迭代後的產品品質,因此大家認為這是個問題,需要好好討論一下。
大家認為能解決的是…
剛剛提到不好的源碼風格會影響迭代後的產品品質,主要的原因就是工程師會受到影響。也就是說「WTF Rate」可以視為和「Bug Spawns Rate」有正相關。因此網路上找到的資料,主要都是認為能解決或優化以下的問題。
幫助理解
這是最多人支持的理由,包括我剛剛舉的例子。有許多同義詞像是「快速進入狀況」,「更快找到重點」或是「看出錯誤邏輯」等等的,我認為這就是同一回事,一起說就行了,因為若你無法理解源碼的目的及流程,我當然不相信你能進入什麼狀況(你唯一能進入的狀況叫「沈思」),更別說還能看出什麼問題。
這裡被提出最多說明是,有同樣的命名風格時,你可以一眼就看出這是個函式,這是個全域變數,這是常數等等的資訊,這和之前匈牙利命令法的理由是一致的。這個理由已有足夠多的觀點被提出是不成立的(不相信的話就連進剛剛那個連結看就行了),因為現在的IDE不但可以完全滿足你的需求,讓你一眼就看出那是什麼,而且還可以告訴你更多你不需要的〝建議〞,但你會發現知道這些資訊並沒有辦法幫助你理解。
真正讓你難以理解的不是風格,而是領域知識(Domain Knowhow)。叫資料庫軟體的來看遊戲程式,或是叫網頁前端的來看Shader,就跟叫吳寶春來看阿基師做菜,用什麼風格做菜他一樣都看不懂。
現在IDE真的很進步了 ,所以相反的也比較不容易看到一些被毀容過的源碼,像是什麼縮排不對的,還是括號都擠在同一列的,所以若你看到的源碼沒有糟到這種程度,接下來是否能有效理解就不是風格問題了。
減少版控系統回報的偽差異
有人認為若是大家的風格不同,就會造成版控系統回報的差異很多都是假的,包括什麼空白/跳格的個數不同,括號的位置不同…能不同的東西就看語言的自由度決定。當然,雖然大家都會有自己的編程風格,有時真的是會手賤去改些小東西,我就是看不慣括號的位置不在這兒,或是我就是覺得空白很難用/很難看之類的,但實務經驗中我真的沒有發現有多大的困擾。那又為什麼這個問題會被提出來呢?
當大家的風格都不同時,只要有人想嘗試要把所有源碼都〝修正〞成同一個風格時,就會把這個差異放大到每個人都看到。
我必須告解一下,我就曾幹過這種事情,基本上若是我再看到真的很可怕的源碼編排的話,我應該還是會幹一樣的事情。不過,有一個很重要的前提是,我不會太龜毛到要求整份源碼一定要全部用空白或是跳格,或是整份源碼的括號一定要放在哪裡,我只會對真的被毀容過的源碼動刀而已(對,我是崇尚自然美的…XD),所以這種狀況真的不常發生。
若很不幸的團隊中龜毛的人很多,大家看到都想自己整型一次,而且溝通的效果不大時,這時只能靠工具了。許多比對工具,都會有一些可忽略的小選項可以設定,像是可以忽略空白/跳格的個數,可以忽略換行,甚至可以自己指定要忽略什麼pattern(如果你熟「正規表達法」的話),所以這實在是工具問題,若不知道的可以找一下,若不能設定的,那該換的是比對工具,不是源碼風格(或是把製造問題的人換掉,風格就統一了…XD)。
改善文件化
透過類似Doxygen這類將註解文件化的工具,可減少一些工程師製作文件的痛苦。號稱是這樣沒錯,但一來它沒有解決真正的痛點:即時描述及更新,二來它也有自己的問題(中文編碼,網頁排版,註解型式等數不清的選項及意義),我用過一兩次後就放棄了。實務上的狀況會更令人沮喪:
如果把函式的實作做完,把說明寫得清楚,還加上範例程式說明。〝只需要做一次的話〞,我也很樂意。但實際上的狀況是我連實作都做不完。
源碼風格也好,文件化的程度也罷,為什麼我們就是不能像室內設計師或建築工程師那樣,自信且自豪的把文件及範例寫好,讓自己的風格揮灑在源碼之中,讓大家都看到我們的水平多麼高端?為什麼我們的〝文采〞就註定被隱藏在產品中,而不像那些小說家或畫家那樣直接被人們看見優美的設計?沒辦法,軟體開發的高迭代頻率,以及最終產品不會(也不能)被看到的特性,都讓源碼風格及文件化永遠是優先權最低的一件事。因此,比起文件化這個需求,更多的要求是:
你源碼的自身可述化(Self Explained)高到一定的程度時,就不需要文件了。
也就是在扣掉語言自己的特性之後,整份源碼的變數命名,介面及實作的分層設計,流程規劃都精煉一個簡潔明瞭的程度時,這份源碼就不需要文件了,任何一個有領域知識的工程師都能很快的進入狀況。
實際上能解決的只是…
實務上,就算我們真正訂定了什麼源碼風格,但我們仍然無法讓第三方的源碼風格也跟我們一致。那些源碼風格其實也很難解決「幫助理解」的問題,更何況現在跨平台的開發需求激增,我們面臨的更不會是同一種語言,我們又要訂定什麼「源碼風格」呢?
我自己的經驗完全是以C/C++為主,有很多名作說了怎麼寫能讓產品更穩健。但這些太實務的經驗只有一小部分能套用到其他語言,俗話說「隔行如隔山」,在軟體開發也是如此,換了個語言其實很多「口音」就不一樣,很多經驗也不能通用。所以我們的思考範圍要擴大到跨語言的程度,完全以實務的取向去訂出方向,而不是綁定在某種語言上的細則。
不管是哪種軟體開發,我們都希望別寫錯東西,就算寫錯也希望能趕快看得出會錯,就算已經是有錯了,也還是希望能整理一下,不要有更多的錯。所以接下來我們應該就是朝這三個方向討論。
不容易寫錯 / 別像算命仙那樣取名
有人做過一個有趣的統計「程式設計師覺得最困難的事」,最困難的榜首不意外就是「幫東西命名」。因為這十分考驗工程師的英文能力及軟體工程實力。一個好的命名,原則倒是挺簡單的:
選字精準到模糊空間最小,多字交疊到全名總長最短,縮寫到能夠唸得出來最好。
我曾經接手一份源碼,雖然是C的實作,但由於工程師是Java出身的,所以帶了Java的嚴重「口音」,變數及函式都長得非常可怕,隨便撈一個都是超過15個字母組出來的複合字,像是「GameSettingMainMenuAbstractionLayer」或是「ActionManagerToggleAutoPlay」這種的。有一天晚上加班較晚,精神不濟,那時還在使用Visual Studio,在指定一個值的時候沒看清楚,就發生了這樣的事情:
ActionManagerTo…(跳出自動完成清單): ActionManagerToggleSound ActionManagerToggleAlphaTest ActionManagerToggleAutoPlay ActionManagerToggleFPSLine ActionManagerToggleDebug (...依此類推…)
然後我就選選選…選到「ActionManagerToggleAlphaTest」我就繼續寫了,但我其實要做的對象是「ActionManagerToggleAutoPlay」。
可想而知,編譯器不會提出任何問題,沒有錯誤,但運行起來就會有很〝詭異〞的現象出來,然後我就花上大把的時間去插log。那次的印象蠻深刻的,因為我甚至在覆審源碼的時候都還是會把「AlphaTest」誤認為是「AutoPlay」呢,直到最後我才突然看到:「唉呀!怎麼是AlphaTest啦,我明明是要寫到AutoPlay的…」。變數一但拉長到你難以一眼全部看完的程度,這就是一個危險的名字。
這種超長的名字多半是因為串上模組名或是功能名搞出來的,就像算命仙那樣,為了怕跟人家撞名,又想要增加辨識度,就把什麼前綴跟後綴都一起給冠上去。缺什麼補什麼,就會搞出連你自己都唸不完也記不得的名字。
另外有一派命名法是仿了之前提到的匈牙利命名法,把這個symbol的軟體角色含入命名之中。像是全域/區域變數加個「m_」,巨集全大寫,常數加個「con」什麼的,這種命名只會給工程師埋下未爆彈。源碼搬來改去是天天在做的事,怎麼會妄想一個變數被加了個「m_」就永遠代表是個成員呢?我搬到外面去編譯器也不會通知你呀。所以千萬別像算命仙那樣想預測未來還是亂補一通,命名就是務求「短小精幹」就好,別妄想〝吉祥〞的名字會帶來什麼特別的好運。
不容易寫錯 / 別吃太多糖
C/C++有特別多〝糖〞可以吃。像是在if或是while中只有一行要執行的時候,可以不加大括號;不乖乖寫邊界處理,而是依賴執行環境來決定char超過127會變成-1,或是int的最大值只有4Byte而不是8Byte;很〝靠勢〞自己運算元順序搞得很清楚,就是懶得寫那麼幾個括號。
另一個問題是著名的「許功蓋」中文編碼問題。中文註解是很好懂,但編碼不同除了會造成版控系統的變更判定,也會為沒有經驗的新人埋下未爆彈。不知道「\」會接下一行特性的新人,有可能就會因此花上一整天去找一個根本不存在的bug。
有的糖是C這個語言的特性,有的則是硬體或作業系統的特性,但無論如何,這些糖都是沒有理由去吃的,都是未爆彈。其他的語言或許也有這些東西,在使用前真的就是要想清楚,你現在這樣寫沒事,換了個系統,換了個開發者,有可能就會爆在一些完全想不通的地方。
不容易寫錯 / 別讓雙胞胎有機會捉弄你
這也是實務經驗常發生的事。 「i」和「j」,「q」和「g」,「9」和「g」,「l」和「1」,「o」和「0」等這些長得很像的字,有時也會讓你的程式發生一些很難理解的問題。最常聽的就是「i」跟「j」或是「q」和「g」,因為都是英文字母,所以都能當變數名。尤其是計數迴圈的變數,一搞錯就會找很久的bug,而這完全是沒有必要且浪費生命的失誤。
所以一個較好的解決方法就是在編輯器上選用等寬的字體,讓「i」跟「j」或是「q」和「g」能被辨識得出來。但最好的解決方法則是,是根本不要在相鄰的源碼區間用上雙胞胎變數,別讓它們有機會捉弄你。
看得出會錯
與其說這是「風格」,還不如說這是個「範式(Paradigm)」。各家語言會有自己發明的一些機制,像是C#的「Coroutine」或是Java的「JNI」等,但扣掉那些語言自身特性的東西,一些變數,函式或類別的概念還是一致的,所以一份好的源碼,它應該總是要維持著一種容易「看得出有錯」的慣例,就像:
- 有變數就要有初始值
- 有參數就要有檢查
- 有回傳值就要馬上給
前兩個還比較好理解,「3.」的意思是這樣的:
int get_person_count() { int rtn = 0; // …(先把回傳值寫好,再寫內容。)... return rtn; }
這意思是說,即便你寫到一半被叫去開會,或是你的源碼交給另一個人維護了,當程式運行起來在這裡有疑慮的時候,你完全能第一時間就反應過來:「我有實作嗎?如果沒有,它必然是傳回0」。
這樣的慣例建立起來之後,再參與源碼覆審時(code review)時,你只要看到這些狀況出來的時候,你就能很快的反應出來這是一個有危險的寫法,很容易〝看得出會錯〞。
寧可重構,也不要架構有錯
這其實不算是風格問題,但卻是個能讓你的源碼〝保鮮〞多久的問題。在錯誤的架構設計下,許多一開始規劃好的源碼到最後都會像整修前的中港大排那樣惡臭連連(因為什麼鬼東西都排到裡面去…),而發臭的源碼必然就會直接影響產品的品質。
架構別錯/有類別就要給角色,想介面
C語言是一種只有結構(Struct)而沒有類別(Class)的語言,因此〝角色〞的觀念並沒有那麼強烈及重要。但目前一線的主流語言,都有類別這樣的資料結構,在設計上就要常常覆審一下,它現在有脫稿演出嗎?在處理網路功能類別的成員函式實作中,它有處理視窗顯示或用戶交互的部分嗎?在遊戲引擎的底層是否有包入了專案的規格,以取得更簡便的處理嗎?我新增了一個新的類別,有新的實作,我能〝明確〞的說出,這個類別的實體是專注在解決什麼問題嗎?
當一個類別被發明出來,它就必須要在整個工作流中有明確的角色。有了明確的角色,才會有明確的介面。如同剛剛舉的例子,或許在某個連線功能的對話框由網路函式庫來顯示,在技術上沒有問題,用戶也不會覺得有什麼問題,但隨著規格開始群魔亂舞(遊戲產品的規格變化之大,這樣形容不會太過份…XD)的時候,你的產品就會開始敗象盡露,同功能的源碼必須在多個類別都複製一份,不同的功能會有同樣的bug,很多很靈異的事情就會開始冒出來。
開立介面這件事,是維持品質中蠻容易被忽略的一件事。在軟體的世界中,工程師就是神,神可以決定要不要讓用戶的許願成真。所以,就像「王牌天神(Bruce Almighty)」中演的那樣,你要是來者不拒,很快的就會出大事。用戶用了你的框架或函式庫,他就有絕對的權力決定是特定的函式是每frame呼叫一次還是每秒呼叫一次;是只呼叫A不呼叫B,還是先呼叫A再呼叫B…各種奇葩的用法組合,會隨著你開出的介面數量成等比級數上升。用了你的框架或函式庫,當然就是你要負責解決用戶的問題。
架構別錯/你只是想要身體,還是想要建立關係?
這標題好聳動啊…別誤會,我指的是「實作」和「從屬關係」。
物件導向設計中,常見的一個問題是「何時該繼承vs何時該組合」。這種問題的詳細回答及舉例都很多,我這邊給同事的一個簡單判斷法通常都蠻管用的:
你只是要它的功能,還是要跟它建立從屬關係?
只是要功能,那就拿它的實體(Instance)來用,要建立關係才考慮繼承為它的子民。
〝雜交〞問題通常都出現在C++,所以的語言都很有默契的拿掉這個能力,但不是代表這個能力有問題。我自己用過那些只能單一繼承的語言,往往就覺得〝組合〞那些功能起來是很沒生產力的事情。我有一本有聲書,打開到第5頁的時候會發出語音說故事給我聽,它為什麼只能從「書」和「多媒體」中選一種重生(spawn)呢?所以想清楚你的需求是什麼,能適當的使用,就能讓產品的品質不但一樣穩固,還更有彈性應付規格的變更。
你有FreeStyle嗎?
我自己的源碼風格可幾乎說是「沒有風格」。什麼區域變數要先用一個〝m_〞當前綴,還是用空白不用跳格的,我都沒有。我的大括號永遠不會在同一列,symbol的名稱清一色就是「aaa_bbb_ccc」全小寫用底線分隔而已。我沒有「FreeStyle」,我只有「FixedStyle」,搭配一些前人的經驗濃縮出來的「Convention」及「Paradigm」能讓我的程式比別人穩健一點,比別人的好懂一點,好維護一點,我想這樣對「解決問題」及「改善流程」應該會比較有幫助吧。