一些常用的程式碼規範

Rick.lz發表於2021-11-03

一些常用的程式碼規範總結

前言

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

下面將從命名,註釋,程式碼風格,程式設計技巧四個維度展開討論

命名

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

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++
			}
		}
	}
	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++
			}
		}
	}
	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 {
				continue
			}
			count++
		}
	}
	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 {
			continue
		}
		count++
	}
	return count
}

4、將部分巢狀邏輯封裝成函式呼叫,以此來減少巢狀

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/一些常用的程式碼規範總結/

相關文章