程式碼規範-對抗軟體複雜度
1、為什麼需要程式碼規範
任何系統性的專案都需要架構設計,而架構設計的核心命題是控制複雜度
。
但隨著專案的不斷迭代,複雜度就會不斷上升,研發效率就會不斷下降。
而程式碼規範正是對抗軟體複雜度
的有效手段,通過約定俗成的規則,降低複雜度,提升研發效能。
從團隊角度來說,統一的程式碼規範有利於減少閱讀成本和理解成本,並且能提高程式碼質量,長期來說,專案的穩定且可維護,也能更好更快速的支撐業務發展。
2、程式碼是怎麼變壞的
2.1、重複的程式碼
同一個功能,在不同的業務域,大家用同一種技術甚至不同技術都去實現了一遍,撇開復用性不說,當我們要去修改底層的邏輯實現時,上層呼叫有一個漏改都是莫大的風險。
2.2、早期有效的決策不再有效
初期,我們有能力把一段程式碼寫的簡潔且邏輯清晰,但是當業務不斷迭代,邏輯就變得越來越複雜,當其他同學來接手時,是改還是不改,改了出問題誰來背鍋?這是一個靈魂的拷問,然後一邊嘆氣一邊新增自己的程式碼。。。
2.3、過早的優化
過早的優化是萬惡之源,為什麼這麼說,百萬和千萬級別日活的架構肯定是不一樣的,架構是需要演進的,如果一開始百萬級別日活就想奔著千萬級別的架構去,不僅脫離了實際的業務需求,還浪費大量的人力物力財力,反而得不償失,從實際出發,適合自己的才是最重要的。
2.4、對合理性沒有苛求
技術方案往往都不是單一的,當我們有「能跑就行」的想法時,就會選擇最簡單粗暴的方案,但是往往這種方案的複用性和擴充套件性都很差,當歷史的浪潮不斷向前推進,就會演變成技術負債,後人一邊嘆氣一邊又在屎山上拉了一泡。
2.5、過度設計
呼叫鏈路就像老奶奶的裹腳布一樣,又臭又長,一層又一層,增加理解成本,降低開發效率。
2.6、沒有設計
沒有設計也相當可怕,一個函式動輒上千行,剪不斷理還亂,然後後來者又是一邊嘆氣一邊。。。
2.7、排期緊
曾經看到這麼一個問題,為什麼大廠屎山也這麼多,高讚的前兩個回答是這麼說的:
- 因為只允許有寫一遍就成的時間
- 因為能用就行,需求都排不過來
進入一個死迴圈,屎山越堆越高。。
3、如何讓程式碼變好
3.1、命名
大到專案名、模組名、包名、對外暴露的介面,小到類名、函式名、變數名、引數名,只要是做開發,我們就逃不過「起名字」這一關。命名的好壞,對於程式碼的可讀性來說非常重要,甚至可以說是起決定性作用的。
3.1.1、命名多長最合適?
有兩種情況,一種命名特別短,對於程式碼的編寫者來說,自己對程式碼的邏輯很清楚,總感覺用什麼樣的命名都可以達意,實際上,對於不熟悉你程式碼的同事來講,可能就不這麼認為了。
另一種,命名很長,覺得命名一定要準確達意,哪怕長一點也沒關係,但是,如果函式、變數的命名很長,那由它們組成的語句就會很長。在程式碼列長度有限制的情況下,就會經常出現一條語句被分割成兩行的情況,這其實會影響程式碼可讀性。
原則上,命名是以能準確達意為目標。多換位思考,以閱讀者的視角去考量命名是否夠直觀。
3.1.2、命名要可讀、可搜尋
什麼是命名可讀,指的是不要用一些特別生僻、難發音的英文單詞來命名,更不要隨意造詞。
雖然我們並不排斥一些獨特的命名方式,但起碼得讓大部分人看一眼就能知道怎麼讀。而生僻、難發音的單詞會嚴重影響交流溝通。
其次是可搜尋,我們在IDE中編寫程式碼的時候,經常會用「關鍵詞聯想」的方法來自動補全和搜尋。比如,鍵入某個物件「.get」,希望IDE返回這個物件的所有get開頭的方法。
3.1.3、行業規範
還有一些行業通用的規範:
- 介面字首加「I」,表示一個Interface。比如IUserService,對應的實現類命名為UserService;
- 彈窗字尾加「Dialog」,表示一個Dialog。比如AppUpdateDialog;
- 工具類字尾加「Utils」,表示一個工具類。比如TrackUtils;
- 等等;
3.1.4、示例
bad:
fun getName(){}
good:
fun getUserName(){}
3.2、註釋
命名很重要,註釋跟命名同等重要。
3.2.1、註釋應該寫什麼?
註釋的目的就是讓程式碼更容易看懂。只要符合這個要求的內容,你就可以將它寫到註釋裡。
比如,闡述程式碼的邏輯,你為什麼這麼做,想要達到什麼樣的效果等等。
3.2.2、註釋是不是越多越好?
註釋太多和太少都有問題。
太多,有可能意味著程式碼寫得不夠可讀,需要寫很多註釋來補充。除此之外,註釋太多也會對程式碼本身的閱讀起到干擾。而且,後期的維護成本也比較高,有時候程式碼改了,註釋忘了同步修改,就會讓程式碼閱讀者更加迷惑。
當然,如果程式碼中一行註釋都沒有,那隻能說明這個程式設計師很懶,我們要適當督促一下,讓他注意新增一些必要的註釋。
3.2.3、註釋類別
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種處理方法:
- 考慮函式是否職責單一,是否能通過拆分成多個函式的方式來減少引數。
- 將函式的引數封裝成物件。
3.4.3、函式設計要職責單一
我們在前面講到單一職責原則的時候,針對的是類、模組這樣的應用物件。實際上,對於函式的設計來說,更要滿足單一職責原則。相對於類和模組,函式的粒度比較小,程式碼行數少,所以在應用單一職責原則的時候,沒有像應用到類或者模組那樣模稜兩可,能多單一就多單一。
整潔的程式碼只做好一件事,乾脆利落,直接了當,易於閱讀,易於維護。
3.4.4、移除過深的巢狀層級
程式碼巢狀層級過深往往是因為if-else、switch-case、for迴圈過度巢狀導致的。我個人建議,巢狀最好不超過兩層,超過兩層之後就要思考一下是否可以減少巢狀。過深的巢狀本身理解起來就比較費勁,除此之外,巢狀過深很容易因為程式碼多次縮排,導致巢狀內部的語句超過一行的長度而折成兩行,影響程式碼的整潔。
針對層級巢狀過深的程式碼可以使用多型簡化邏輯,移除不必要的if或else,也可以使用策略模式,提前return退出巢狀等。
3.4.5、少即是多
無形裝逼最為致命:
- 過度設計:有的同學為了炫技,各種設計模式咔咔往上整,反而增加複雜度;
- 隱式耦合:這種是想炫技但功力不夠的,設計的不夠優雅反而留下後遺症;
建築師米斯.凡德洛曾說過,less is more,提倡簡單,反對度裝飾的設計理念。簡單的東西往往帶給人們的是更多的享受。
4、客戶端的技術棧
上面介紹了一些通用的規範,然而時代在變化,技術在演進,客戶端的技術更新也是日新月異,因此也需要針對不同的技術棧有系統性的規約及程式碼風格。
比如:
- Java的檔名遵循駝峰命名法,而在Flutter中檔名使用下劃線隔開;
- Java和OC是強型別語言,Swift和Kotlin是弱型別語言,不僅有型別推導上的區別,還有一些語法糖的特性;
- 等等;
下面是端上現有的一些技術棧:
| 端 | 語言 | 規範文件 | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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之類的檢測工具,在編寫階段通過警告,把不符合規範的程式碼扼殺在搖籃裡。
5.2、程式碼評審
程式碼評審也稱code review,俗稱cr,cr的意義在於,作為局中人,儘管我們在編寫程式碼的時候小心翼翼,但也可能會在無意間犯下一個小錯誤,而此時cr的人作為旁觀者,可有一語點醒夢中人的效果,而且從實際問題出發產生思想的碰撞,相互學習,也有利於提升團隊整體的編碼水平。
5.3、掃碼行動
我們端上正在做一些程式碼的治理,刪除無用程式碼,下線老程式碼,比如原有的灰度校驗在全量之後理應下線老的邏輯程式碼。
5.4、程式碼重構
我們也正在做模組化的重構治理,把以前設計不合理或者不滿足現狀訴求的地方做改進和優化。
6、一點思考
治理行動我們可以一年來一次,但這很明顯不是最好的解決辦法,程式碼是人寫的,工具是輔助是底線,如何長治久安,還是要從根源上著手下功夫,這就需要我們團結一致,從思想上認可,從行動上落實,認真做好code review,始終對程式碼保持敬畏,對自己的程式碼負責,做一個有信仰有追求的程式設計師。
7、相關書籍
8、參考文件
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
- 【yechaoa】2022進階成長,揚帆再起航!
- 【Gradle-5】Gradle常用命令與引數
- 【Gradle-2】一文搞懂Gradle配置
- 【yechaoa】5年Android開發的2021年終總結,實現Flag的一年
- 程式碼規範-對抗軟體複雜度
- 【建議收藏】17個XML佈局小技巧
- 【造輪子】自定義一個隨意拖拽可吸邊的懸浮View
- 怎麼簡單實現選單拖拽排序的功能
- 【保姆級】包體積優化教程
- Android通知Notification使用全解析,看這篇就夠了
- 【首發】根據桌布修改App主題,它真的來了
- Android原生TabLayout使用全解析,看這篇就夠了
- 【漲姿勢】你沒用過的BadgeDrawable
- Android包體積優化(常規、進階、極致)
- Android Studio Arctic Fox | 2020.3.1、Gradle 7.0升級記錄
- Android 11適配指南之Toast解析
- Android 自定義View之隨機數驗證碼(仿寫鴻洋)
- Jetpack之Room的使用,結合Flow
- Android MediaPlayer音訊播放器詳解
- Android 修改系統音量及監聽