一些常用的代碼規范


一些常用的代碼規范總結

前言

最近在看王爭大佬的設計模式之美,里面談到了代碼規范,剛好也是我平時比較注意的一些點,這里做了一個總結。

下面將從命名,注釋,代碼風格,編程技巧四個維度展開討論

命名

選取一個合適的命名有時候確實是很難的,來看下有哪些可以幫我我們命名的技巧

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/一些常用的代碼規范總結/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM