一些常用的代码规范总结
前言
最近在看王争大佬的设计模式之美,里面谈到了代码规范,刚好也是我平时比较注意的一些点,这里做了一个总结。
下面将从命名,注释,代码风格,编程技巧四个维度展开讨论
命名
选取一个合适的命名有时候确实是很难的,来看下有哪些可以帮我我们命名的技巧
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