程式碼規範-對抗軟體複雜度

語言: CN / TW / HK

1、為什麼需要程式碼規範

任何系統性的專案都需要架構設計,而架構設計的核心命題是控制複雜度

但隨著專案的不斷迭代,複雜度就會不斷上升,研發效率就會不斷下降。

而程式碼規範正是對抗軟體複雜度的有效手段,通過約定俗成的規則,降低複雜度,提升研發效能。

從團隊角度來說,統一的程式碼規範有利於減少閱讀成本和理解成本,並且能提高程式碼質量,長期來說,專案的穩定且可維護,也能更好更快速的支撐業務發展。

2、程式碼是怎麼變壞的

2.1、重複的程式碼

同一個功能,在不同的業務域,大家用同一種技術甚至不同技術都去實現了一遍,撇開復用性不說,當我們要去修改底層的邏輯實現時,上層呼叫有一個漏改都是莫大的風險。

2.2、早期有效的決策不再有效

初期,我們有能力把一段程式碼寫的簡潔且邏輯清晰,但是當業務不斷迭代,邏輯就變得越來越複雜,當其他同學來接手時,是改還是不改,改了出問題誰來背鍋?這是一個靈魂的拷問,然後一邊嘆氣一邊新增自己的程式碼。。。

2.3、過早的優化

過早的優化是萬惡之源,為什麼這麼說,百萬和千萬級別日活的架構肯定是不一樣的,架構是需要演進的,如果一開始百萬級別日活就想奔著千萬級別的架構去,不僅脫離了實際的業務需求,還浪費大量的人力物力財力,反而得不償失,從實際出發,適合自己的才是最重要的。

2.4、對合理性沒有苛求

技術方案往往都不是單一的,當我們有「能跑就行」的想法時,就會選擇最簡單粗暴的方案,但是往往這種方案的複用性和擴充套件性都很差,當歷史的浪潮不斷向前推進,就會演變成技術負債,後人一邊嘆氣一邊又在屎山上拉了一泡。

2.5、過度設計

呼叫鏈路就像老奶奶的裹腳布一樣,又臭又長,一層又一層,增加理解成本,降低開發效率。

2.6、沒有設計

沒有設計也相當可怕,一個函式動輒上千行,剪不斷理還亂,然後後來者又是一邊嘆氣一邊。。。

2.7、排期緊

曾經看到這麼一個問題,為什麼大廠屎山也這麼多,高讚的前兩個回答是這麼說的:

  1. 因為只允許有寫一遍就成的時間
  2. 因為能用就行,需求都排不過來

進入一個死迴圈,屎山越堆越高。。

3、如何讓程式碼變好

3.1、命名

大到專案名、模組名、包名、對外暴露的介面,小到類名、函式名、變數名、引數名,只要是做開發,我們就逃不過「起名字」這一關。命名的好壞,對於程式碼的可讀性來說非常重要,甚至可以說是起決定性作用的。

3.1.1、命名多長最合適?

有兩種情況,一種命名特別短,對於程式碼的編寫者來說,自己對程式碼的邏輯很清楚,總感覺用什麼樣的命名都可以達意,實際上,對於不熟悉你程式碼的同事來講,可能就不這麼認為了。

另一種,命名很長,覺得命名一定要準確達意,哪怕長一點也沒關係,但是,如果函式、變數的命名很長,那由它們組成的語句就會很長。在程式碼列長度有限制的情況下,就會經常出現一條語句被分割成兩行的情況,這其實會影響程式碼可讀性。

原則上,命名是以能準確達意為目標。多換位思考,以閱讀者的視角去考量命名是否夠直觀。

3.1.2、命名要可讀、可搜尋

什麼是命名可讀,指的是不要用一些特別生僻、難發音的英文單詞來命名,更不要隨意造詞。

雖然我們並不排斥一些獨特的命名方式,但起碼得讓大部分人看一眼就能知道怎麼讀。而生僻、難發音的單詞會嚴重影響交流溝通。

其次是可搜尋,我們在IDE中編寫程式碼的時候,經常會用「關鍵詞聯想」的方法來自動補全和搜尋。比如,鍵入某個物件「.get」,希望IDE返回這個物件的所有get開頭的方法。

3.1.3、行業規範

還有一些行業通用的規範:

  1. 介面字首加「I」,表示一個Interface。比如IUserService,對應的實現類命名為UserService;
  2. 彈窗字尾加「Dialog」,表示一個Dialog。比如AppUpdateDialog;
  3. 工具類字尾加「Utils」,表示一個工具類。比如TrackUtils;
  4. 等等;

3.1.4、示例

bad:

fun getName(){}

good:

fun getUserName(){}

3.2、註釋

命名很重要,註釋跟命名同等重要。

3.2.1、註釋應該寫什麼?

註釋的目的就是讓程式碼更容易看懂。只要符合這個要求的內容,你就可以將它寫到註釋裡。

比如,闡述程式碼的邏輯,你為什麼這麼做,想要達到什麼樣的效果等等。

3.2.2、註釋是不是越多越好?

註釋太多和太少都有問題。

太多,有可能意味著程式碼寫得不夠可讀,需要寫很多註釋來補充。除此之外,註釋太多也會對程式碼本身的閱讀起到干擾。而且,後期的維護成本也比較高,有時候程式碼改了,註釋忘了同步修改,就會讓程式碼閱讀者更加迷惑。

當然,如果程式碼中一行註釋都沒有,那隻能說明這個程式設計師很懶,我們要適當督促一下,讓他注意新增一些必要的註釋。

3.2.3、註釋類別

image.png

3.2.4、示例

bad:

/** * 取消 */ protected fun cancelJob(job: Job?) { if (job != null && job.isActive && !job.isCompleted && !job.isCancelled) { job.cancel() } }

good:

/** * 取消協程 會丟擲CancellationException * @param job 協程job */ protected fun cancelJob(job: Job?) { if (job != null && job.isActive && !job.isCompleted && !job.isCancelled) { job.cancel() } }

3.3、程式碼風格

3.3.1、函式、類多大才合適?

函式的程式碼行數不要超過一螢幕的大小,比如50行。

3.3.2、一行程式碼多長最合適?

最好不要超過IDE的顯示寬度。當然,也不能太小,否則會導致很多稍微長點的語句被折成兩行,也會影響到程式碼的整潔,不利於閱讀。

3.3.3、善用空行分割單元塊

對於比較長的函式,為了讓邏輯更加清晰,可以使用空行來分割各個程式碼塊。

除此之外,在類的成員變數與函式之間、靜態成員變數與普通成員變數之間、各函式之間、甚至各成員變數之間,我們都可以通過新增空行的方式,讓這些不同模組的程式碼之間,界限更加明確。寫程式碼就類似寫文章,善於應用空行,可以讓程式碼的整體結構看起來更加有清晰、有條理。

3.3.4、格式化

使用統一的格式化規則,比如空格、換行等,格式化規則不統一,容易引起不必要的變更,不利於程式碼評審和歷史變更查詢。

3.3.5、示例

bad:

AlertDialog.Builder(context).setView(0).setTitle(R.string.dialog_title).setMessage(R.string.dialog_message).setIcon(0) .create()

good:

AlertDialog.Builder(context) .setView(0) .setTitle(R.string.dialog_title) .setMessage(R.string.dialog_message) .setIcon(0) .create()

3.4、編碼技巧

3.4.1、把程式碼分割成更小的單元塊

大部分人閱讀程式碼的習慣都是,先看整體再看細節。所以,我們要有模組化和抽象思維,善於將大塊的複雜邏輯提煉成類或者函式,遮蔽掉細節,讓閱讀程式碼的人不至於迷失在細節中,這樣能極大地提高程式碼的可讀性。不過,只有程式碼邏輯比較複雜的時候,我們其實才建議提煉類或者函式。畢竟如果提煉出的函式只包含兩三行程式碼,在閱讀程式碼的時候,還得跳過去看一下,這樣反倒增加了閱讀成本。

3.4.2、避免函式引數過多

我個人覺得,函式包含3、4個引數的時候還是能接受的,大於等於5個的時候,我們就覺得引數有點過多了,會影響到程式碼的可讀性,使用起來也不方便。

一般有2種處理方法:

  1. 考慮函式是否職責單一,是否能通過拆分成多個函式的方式來減少引數。
  2. 將函式的引數封裝成物件。

3.4.3、函式設計要職責單一

我們在前面講到單一職責原則的時候,針對的是類、模組這樣的應用物件。實際上,對於函式的設計來說,更要滿足單一職責原則。相對於類和模組,函式的粒度比較小,程式碼行數少,所以在應用單一職責原則的時候,沒有像應用到類或者模組那樣模稜兩可,能多單一就多單一。

整潔的程式碼只做好一件事,乾脆利落,直接了當,易於閱讀,易於維護。

3.4.4、移除過深的巢狀層級

程式碼巢狀層級過深往往是因為if-else、switch-case、for迴圈過度巢狀導致的。我個人建議,巢狀最好不超過兩層,超過兩層之後就要思考一下是否可以減少巢狀。過深的巢狀本身理解起來就比較費勁,除此之外,巢狀過深很容易因為程式碼多次縮排,導致巢狀內部的語句超過一行的長度而折成兩行,影響程式碼的整潔。

針對層級巢狀過深的程式碼可以使用多型簡化邏輯,移除不必要的if或else,也可以使用策略模式,提前return退出巢狀等。

3.4.5、少即是多

無形裝逼最為致命:

  1. 過度設計:有的同學為了炫技,各種設計模式咔咔往上整,反而增加複雜度;
  2. 隱式耦合:這種是想炫技但功力不夠的,設計的不夠優雅反而留下後遺症;

建築師米斯.凡德洛曾說過,less is more,提倡簡單,反對度裝飾的設計理念。簡單的東西往往帶給人們的是更多的享受。

4、客戶端的技術棧

上面介紹了一些通用的規範,然而時代在變化,技術在演進,客戶端的技術更新也是日新月異,因此也需要針對不同的技術棧有系統性的規約及程式碼風格。

比如:

  1. Java的檔名遵循駝峰命名法,而在Flutter中檔名使用下劃線隔開;
  2. Java和OC是強型別語言,Swift和Kotlin是弱型別語言,不僅有型別推導上的區別,還有一些語法糖的特性;
  3. 等等;

下面是端上現有的一些技術棧:

| | 語言 | 規範文件 | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Android | Java | Java開發手冊(嵩山版)Google Java Style Guide | | Android | Kotlin | Kotlin Coding conventions | | | iOS | Objective-C | Coding Guidelines for Cocoa | | iOS | Swift | Swift Style Guide | | | 跨端 | Flutter | Effective Dart | | 跨端 | 動態化xxx(json -> TypeScript) | Google TypeScript Style Guide | | | 其他 | 配置檔案(xml、yml,json) | Google XML Document Format Style Guide | | 其他 | 指令碼、外掛(Python、Shell、Groovy) | Shell Style GuidePython Style Guide | |

google開源規約:https://google.github.io/styleguide/

5、治理手段

5.1、檢測工具

通過一些類似Lint之類的檢測工具,在編寫階段通過警告,把不符合規範的程式碼扼殺在搖籃裡。

Alibaba Java Coding Guidelines

5.2、程式碼評審

程式碼評審也稱code review,俗稱cr,cr的意義在於,作為局中人,儘管我們在編寫程式碼的時候小心翼翼,但也可能會在無意間犯下一個小錯誤,而此時cr的人作為旁觀者,可有一語點醒夢中人的效果,而且從實際問題出發產生思想的碰撞,相互學習,也有利於提升團隊整體的編碼水平。

5.3、掃碼行動

我們端上正在做一些程式碼的治理,刪除無用程式碼,下線老程式碼,比如原有的灰度校驗在全量之後理應下線老的邏輯程式碼。

5.4、程式碼重構

我們也正在做模組化的重構治理,把以前設計不合理或者不滿足現狀訴求的地方做改進和優化。

6、一點思考

治理行動我們可以一年來一次,但這很明顯不是最好的解決辦法,程式碼是人寫的,工具是輔助是底線,如何長治久安,還是要從根源上著手下功夫,這就需要我們團結一致,從思想上認可,從行動上落實,認真做好code review,始終對程式碼保持敬畏,對自己的程式碼負責,做一個有信仰有追求的程式設計師。

7、相關書籍

8、參考文件

本文正在參加「金石計劃 . 瓜分6萬現金大獎」