一些常用的代碼規范總結
前言
最近在看王爭大佬的設計模式之美,里面談到了代碼規范,剛好也是我平時比較注意的一些點,這里做了一個總結。
下面將從命名,注釋,代碼風格,編程技巧四個維度展開討論
命名
選取一個合適的命名有時候確實是很難的,來看下有哪些可以幫我我們命名的技巧
1、命名的長度選擇
關於命名長度,在能夠表達含義的額情況下,命名當然是越短越好。在大多數的情況下,短的命名不如長的命名更能表達含義,很多書籍是不推薦使用縮寫的。
盡管長的命名可以包含更多的信息,更能准確直觀地表達意圖,但是,如果函數、變量的命名很長,那由它們組成的語句就會很長。在代碼列長度有限制的情況下,就會經常出現一條語句被分割成兩行的情況,這其實會影響代碼可讀性。
所以有時候我們是可以適量的使用縮寫的短命名
在什么場景下合適使用短命名
1、對於一些默認,大家都熟知的倒是可以使用縮寫的命名,比如,sec 表示 second、str 表示 string、num 表示 number、doc 表示 document 等等
2、對於作用域比較小的變量,我們可以使用相對短的命名,比如一些函數內的臨時變量,相對應的對於作用於比較大的,更推薦使用長命名
2、利用上下文簡化命名
來看個栗子
type User struct {
UserName string
UserAge string
UserAvatarUrl string
}
比如這個struct,我們已經知道這是一個 User 信息的 struct。里面用戶的 name ,age,就沒有必要加上user的前綴了
修稿后的
type User struct {
Name string
Age string
AvatarUrl string
}
當然這個在數據庫的設計中也是同樣有用
3、命名要可讀、可搜索
“可讀”,指的是不要用一些特別生僻、難發音的英文單詞來命名。
我們在IDE中編寫代碼的時候,經常會用“關鍵詞聯想”的方法來自動補全和搜索。比如,鍵入某個對象“.get”,希望IDE返回這個對象的所有get開頭的方法。再比如,通過在IDE搜索框中輸入“Array”,搜索JDK中數組相關的函數和方法。所以,我們在命名的時候,最好能符合整個項目的命名習慣。大家都用“selectXXX”表示查詢,你就不要用“queryXXX”;大家都用“insertXXX”表示插入一條數據,你就要不用“addXXX”,統一規約是很重要的,能減少很多不必要的麻煩。
4、如何命名接口
對於接口的命名,一般有兩種比較常見的方式。一種是加前綴“I”,表示一個Interface。比如IUserService,對應的實現命名為UserService。另一種是不加前綴,比如UserService,對應的實現加后綴“Impl”,比如UserServiceImpl。
注釋
我們接受一個項目的時候,經常會吐槽老項目注釋不好,文檔不全,那么如果注釋都讓我們去寫,怎樣的注釋才是好的注釋
有時候我們會在書籍或一些博客中看到,如果好的命名是不需要注釋的,也就是代碼即注釋,如果需要注釋了,就是代碼的命名不好了,需要在命名中下功夫。
這種是有點極端了,命名再好,畢竟有長度限制,不可能足夠詳盡,而這個時候,注釋就是一個很好的補充。
1、注釋到底該寫什么
我們寫數注釋的目的是讓代碼更易懂,注釋一般包括三個方面,做什么、為什么、怎么做。
這是 golang 中 sync.map中的注釋,也是分別從做什么、為什么、怎么做 來進行注釋
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
有些人認為,注釋是要提供一些代碼沒有的額外信息,所以不要寫“做什么、怎么做”,這兩方面在代碼中都可以體現出來,只需要寫清楚“為什么”,表明代碼的設計意圖即可。
不過寫了注釋可能有以下幾個優點
1、注釋比代碼承載的信息更多
函數和變量如果命名得好,確實可以不用再在注釋中解釋它是做什么的。但是,對結構體來說,包含的信息比較多,一個簡單的命名就不夠全面詳盡了。這個時候,在注釋中寫明“做什么”就合情合理了。
2、注釋起到總結性作用、文檔的作用
在注釋中,關於具體的代碼實現思路,我們可以寫一些總結性的說明、特殊情況的說明。這樣能夠讓閱讀代碼的人通過注釋就能大概了解代碼的實現思路,閱讀起來就會更加容易。
3、一些總結性注釋能讓代碼結構更清晰
對於邏輯比較復雜的代碼或者比較長的函數,如果不好提煉、不好拆分成小的函數調用,那我們可以借助總結性的注釋來讓代碼結構更清晰、更有條理。
2、注釋是不是越多越好
注釋本身有一定的維護成本,所以並非越多越好。結構體和函數一定要寫注釋,而且要寫得盡可能全面、詳細,而函數內部的注釋要相對少一些,一般都是靠好的命名、提煉函數、解釋性變量、總結性注釋來提高代碼可讀性。
代碼風格
1、函數多大才合適
函數的代碼太多和太少,都是不太好的
太多了:
一個方法上千行,一個函數幾百行,邏輯過於繁雜,閱讀代碼的時候,很容易就會看了后面忘了前面
太少了:
在代碼總量相同的情況下,被分割成的函數就會相應增多,調用關系就會變得更復雜,閱讀某個代碼邏輯的時候,需要頻繁地在n多方法或者n多函數之間跳來跳去,閱讀體驗也不好。
多少最合適的呢?
不過很難給出具體的值,有的地方會講,那就是不要超過一個顯示屏的垂直高度。比如,在我的電腦上,如果要讓一個函數的代碼完整地顯示在IDE中,那最大代碼行數不能超過50。
2、一行代碼多長最合適
這個也沒有一個完全的准側,畢竟語言不同要求也是不同的
當然有個通用的原則:一行代碼最長不能超過IDE顯示的寬度。
太長了就不方便代碼的閱讀了
3、善用空行分割單元塊
也就是垂直留白,不太建議我們的代碼寫下來,一個函數或方法中一行空格也沒余,通常會根據不同的語義,一個小模塊的內容完了,通過空白空格進行分割。
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
// ...
m.mu.Unlock()
}
這里上鎖的代碼就和上文進行了空格
當然有的地方會講首行不空格,這也是對的,函數頭部的空行是沒有任何用的。
編程技巧
1、把代碼分割成更小的單元塊
善於將代碼中的模塊進行抽象,能夠方便我們的閱讀
所以,我們要有模塊化和抽象思維,善於將大塊的復雜邏輯提煉成小的方法或函數,屏蔽掉細節,讓閱讀代碼的人不至於迷失在細節中,這樣能極大地提高代碼的可讀性。不過,只有代碼邏輯比較復雜的時候,我們其實才建議把對應的邏輯提煉出來。
2、避免函數或方法參數過多
函數包含3、4個參數的時候還是能接受的,大於等於5個的時候,我們就覺得參數有點過多了,會影響到代碼的可讀性,使用起來也不方便。
針對這種情況有兩種處理方法
1、考慮函數是否職責單一,是否能通過拆分成多個函數的方式來減少參數。
2、將函數的參數封裝成對象。
栗子
func updateBookshelf(userId, deviceId string, platform, channel, step int) {
// ...
}
// 修改后
type UpdateBookshelfInput struct {
UserId string
DeviceId string
Step int
Platform int
Channel int
}
func updateBookshelf(input *UpdateBookshelfInput) {
// ...
}
3、勿用函數參數來控制邏輯
不要在函數中使用布爾類型的標識參數來控制內部邏輯,true的時候走這塊邏輯,false的時候走另一塊邏輯。這明顯違背了單一職責原則和接口隔離原則。
可以拆分成兩個函數分別調用
栗子
func sendVip(userId string, isNewUser bool) {
// 是新用戶
if isNewUser {
// ...
} else {
// ...
}
}
// 修改后
func sendVip(userId string) {
// ...
}
func sendNewUserVip(userId string) {
// ...
}
不過,如果函數是private私有函數,影響范圍有限,或者拆分之后的兩個函數經常同時被調用,我們可以酌情考慮不用拆分。
4、函數設計要職責單一
對於函數的設計我們也要盡量職責單一,避免設計一個大而全的函數,可以根據不同的功能點,對函數進行拆分。
舉個栗子:我們來校驗下我們的額一些用戶屬性,當然這個校驗就省略成判斷是否為空了
func validate(name, phone, email string) error {
if name == "" {
return errors.New("name is empty")
}
if phone == "" {
return errors.New("phone is empty")
}
if email == "" {
return errors.New("name is empty")
}
return nil
}
修改過就是
func validateName(name string) error {
if name == "" {
return errors.New("name is empty")
}
return nil
}
func validatePhone( phone string) error {
if phone == "" {
return errors.New("phone is empty")
}
return nil
}
func validateEmail(name, phone, email string) error {
if email == "" {
return errors.New("name is empty")
}
return nil
}
5、移除過深的嵌套層次
代碼嵌套層次過深往往是因為if-else、switch-case、for循環過度嵌套導致的。過深的嵌套,代碼除了不好理解外,嵌套過深很容易因為代碼多次縮進,導致嵌套內部的語句超過一行的長度而折成兩行,影響代碼的整潔。
對於嵌套代碼的修改,大概有四個方向可以考慮
舉個栗子:
這段代碼中,有些地方是不太合適的,我們從下面的四個方向來分析
func sum(sil []*User, age int) int {
count := 0
if len(sil) == 0 || age == 0 {
return count
} else {
for _, item := range sil {
if item.Age > age {
count++
} else {
// do something
// ....
}
}
}
return count
}
1、去掉多余的if或else語句
修改為
func sum(sil []*User, age int) int {
count := 0
if len(sil) != 0 && age == 0 {
for _, item := range sil {
if item.Age > age {
count++
} else {
// do something
// ....
}
}
}
return count
}
2、使用編程語言提供的continue、break、return關鍵字,提前退出嵌套
func sum(sil []*User, age int) int {
count := 0
if len(sil) != 0 && age == 0 {
for _, item := range sil {
if item.Age > age {
count++
continue
}
// do something
// ....
}
}
return count
}
3、調整執行順序來減少嵌套
func sum(sil []*User, age int) int {
count := 0
if len(sil) == 0 || age == 0 {
return count
}
for _, item := range sil {
if item.Age > age {
count++
continue
}
// do something
// ....
}
return count
}
4、將部分嵌套邏輯封裝成函數調用,以此來減少嵌套
func sum(sil []*User, age int) int {
count := 0
if len(sil) == 0 || age == 0 {
return count
}
for _, item := range sil {
if item.Age > age {
count++
continue
}
dealUser(item, age)
}
return count
}
func dealUser(user *User, age int) {
if user.Age > age {
return
}
// do something
// ....
}
6、學會使用解釋性變量
常用的用解釋性變量來提高代碼的可讀性的情況有下面2種
1、常量取代魔法數字
func CalculateCircularArea(radius float64) float64 {
return 3.1415 * radius * radius
}
// 修改后
const PI = 3.1415
func CalculateCircularArea(radius float64) float64 {
return PI * radius * radius
}
2、使用解釋性變量來解釋復雜表達式
if appOnlineTime.Before(userId.Timestamp()) {
appOnlineTime = userId.Timestamp()
}
// 修改后
isBeforeRegisterTime := appOnlineTime.Before(userId.Timestamp())
if isBeforeRegisterTime {
appOnlineTime = userId.Timestamp()
}
參考
【設計模式之美】https://time.geekbang.org/column/intro/100039001
【一些常用的代碼規范總結】https://boilingfrog.github.io/2021/11/03/一些常用的代碼規范總結/