代码整洁之道

整洁代码 #

简单代码,根据重要顺序应该是:

  • 能通过所有测试
  • 没有重复代码
  • 体现系统中的全部设计理念
  • 包括尽量少的实体,比如方法、函数等

光写好代码是不够的,必须时时刻刻保持整洁。

有意义的命名 #

1、命名要名副其实,直接体现要做的事情。

openFile-->openVideo

2、选择一个好名字要花时间,但省下来的时间比花掉的多。一旦发现有更好的名称,就换掉旧的。

3、避免使用会引起误解的名称,尤其是遇到一些缩写,要防止其存在歧义。

//反面例子
type Std struct {
    Name string
    Age  int
    Cls  string
}

std 可能被误解为 Standard,而实际上它应该表示 Student(学生)。

4、不要去做无意义的区分,例如:taskDatataskInfo,名称虽然不同但意义没有差别。

  • variable一词不应当出现在变量名中,table一词不应该出现在表名中。例如:NameString不可取,名称后面建议不要加类型。

    //反面例子
    var recordMap map[int64]*RecordDbManager
    
  • 总之在读者能区分的情况下、不产生歧义的情况下,越简单的命名就是好命名。

5、使用可以读出来的命名。

  • generationTimestamp要比genymdhms要好的多,即使前一个比较长一点,但是无所谓,表达准确意思。

6、使用可以搜索的名字

  • 长名称胜于短名称,搜得到的名称比自编代码写就的名称要好。

    //反面例子
    for i=0i<30;i++{
    	s+=t[i]*4/5
    }
    //4 和 5代表什么意思要用常量说明,否则维护人员很难注意
    const WorkDays int =5
    const realDays int =4
    

7、命名不要有无意义的前缀,人们只会看到名称中有意义的部分。

//反面例子
var m_student string

8、避免思维映射。例如:循环计数通常使用i,j,k,千万别用l 。专业的程序员编写其他人能理解的代码

//反面例子
for l=0l<30;l++{
	s+=t[l]*4/5
}

9、结构体命名不应该用动词,方法命名应该用动词,可加上get, is ,set 前缀。

10、别做一些没有意义的聪明举动,宁可明确,毋为好玩言到意到,意到言到

11、每个概念对应一个词,在项目中若出现自定义方法命名,必须贯穿整个项目。例如getTask,getEvidence….不能后面出现fetchEvidence。函数名称应该独一无二,并且保持一致。 ———统一

12、别用同一个词表达不同的概念,做到”一词一义“。

  • // 这个函数名称不明确,可能会引起混淆
    func open(file string) (*os.File, error) {
        return os.Open(file) // 这里的打开操作实际是读取文件
    }
    
    // 另一个函数也使用了“open”这个词,但实际是创建新文件
    func open(file string) (*os.File, error) {
        return os.Create(file) // 这里的打开操作实际是创建文件
    }
    

13、不要用专业领域中的名字,比如项目是基因学相关,不要用里面的专业名词去命名。

14、如果不能用程序员熟悉的术语来给手头的工作命名,就采用所涉及问题领域而来的名称。

15、大多名称无法自我说明,你需要用良好的结构体、函数来放置名称,给作者提供语境,或者给名字加前缀。不要怕代码变多,意思明确是最主要的。

  • //例如:
    func process(data string) {
        // 处理订单数据的逻辑
    }
    
    type Info struct {
        a string
        b float64
    }
    
    // 函数处理订单数据,提供了明确的语境信息
    func processOrderData(orderData string) {
        // 处理订单数据的逻辑
    }
    
    // 结构体用于表示订单信息,提供了明确的语境信息
    type OrderInfo struct {
        customerName  string
        orderTotal    float64
        orderDate     string
    }
    

16、不要添加没用的语境,只要短名称足够清楚,就比长名称好

17、经常试着去更改命名,让它更贴切

函数 #

1、函数就应该短小,每个函数都一目了然,每个函数都只做一件事情,每个函数都依次把你带到下一个函数,这就是函数应该达到的最短小程度。代码变多了无所谓

2、**函数应该做一件事,做好这件事,只做这一件事。**要判断函数是否只做一件事,就看它是否还能拆分。

3、每一个函数一个抽象层

  • 比如我们需要从一个整数数组中找到最大的数,并计算它的平方。我们可以将这个需求分解为两个抽象层次:找到数组中的最大数,然后计算它的平方。每一个函数都只关注一个抽象层次的工作,使代码更加清晰易于理解。

4、使用具有描述性的名称

  • 不要怕函数名称太长,要贴切
  • 不要怕取名时间太久,要贴切
  • 命名方式要一致,使用一脉相承的短语、名词、动词给函数命名。

5、函数参数

  • 最理想的函数参数数量是0,其次是1,再次是2,尽量避免出现3参数,除非你有足够的理由。

  • 尽量不向函数传入布尔值,因为会让函数功能变得不单一,违背函数只做一件事原则。

    //反面例子
    func (fp *FileProxy) Info(cid int64, eid int64, fullPath string, isCaseSensitive bool) (resultData []*file.File, err error) {}
    
  • 能用单参数,就不用双参数,否则就拆分函数。三参数也一样,当参数过多时,建议用结构体。

  • 对于单参数函数,函数和参数应该形成一种非常良好的动词/名词对形式。

    //例如
    write(name)
    writeField(name)//更好
    

6、函数要么做什么事,要么回答什么事,二者不可兼得。两样都干,常会显得混乱。

7、函数只做一件事,要避免重复。

8、函数中尽量避免使用如下操作 break outerLoop

  • //反面典型
    outerLoop:
    	for {
    		_, ok := <-ticker.C
    		if ok {
    			tasks, err := api.GetTasks(api.GetCaseId(), api.GetEvidenceId(), "", "")
    			if err != nil {
    				common.Log.Error(err.Error())
    				break
    			}
    			i++
    			for _, task := range tasks {
    				if task.Type == "scan" {
    					if _, exit := executed[task.Name]; !exit { 
    
    						......
    
    						if task.Name == "deepscan" { 
    							break outerLoop
    						}
    						break
    					}
    				}
    			}
    		}
    	}
    

9、如何写出功能单一的函数————先写长函数,不断拆分

大师级的程序员把系统当作故事来讲,而不是当作程序来写。

注释 #

1、如果你需要给这段代码加注释,那么你需要重写这段代码。与其花时间去注释那段糟糕的代码,不如花时间去清理那段代码。

2、注意力放到代码上,能不用注释就不用注释,用代码去表达。

3、尽管有时也需要注释,但我们应该多花心思去减少注释。不准确的注释,比不注释要好得多。

4、什么是好注释

  • 好的注释应当解释代码的意图、背景信息或复杂逻辑。

    /*
    ImTreeNode 该结构应用于即时通讯类插件解析
    
    该结构的特性有:
    1.节点名称自动拼接,例:张三(123456);
    2.根节点自动统计账号详情;
    3.账号节点自动整理详情页;
    4.聊天消息节点默认按最后聊天时间排序;
    5.聊天数据自动统计,自动生成聊天消息分类节点;
    6.自动格式化数据,校验提交数据,修正或提醒部分不必要的错误;
    */
    type ImTreeNode struct {
    	*TreeNode
    	accId          string
    	statisticsFunc StatisticsFunc // 数据统计方法
    	setAccIdFunc   SetAccIdFunc   // 设置数据统计账号id
    }
    
  • 有些注释是必须的,也是有利的。但要记住,唯一正真的好做法是想办法不写注释

  • 规范要求编写的注释

    // © 2024 Acme Corp. All rights reserved.
    // Licensed under the Apache License, Version 2.0 (the "License");
    // you may not use this file except in compliance with the License.
    // You may obtain a copy of the License at
    //
    //     http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing, software
    // distributed under the License is distributed on an "AS IS" BASIS,
    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    // See the License for the specific language governing permissions and
    // limitations under the License.
    
  • 提供信息的注释

    /*
    IsFileExist 判断文件或路径是否存在
    
    path: 文件路径,不区分挂载点路径和磁盘路径
    */
    func IsFileExist(filePath string) bool {}
    
  • 解释意图的注释

    // findMax 查找整数数组中的最大值
    // 使用了手动循环而不是内置的 max 函数
    // 目的是为了练习循环和条件判断
    func findMax(numbers []int) int {
    	max := numbers[0]
    	for _, num := range numbers {
    		if num > max {
    			max = num
    		}
    	}
    	return max
    }
    
  • 注释把某些晦涩难懂的参数或返回值的意义翻译为某种可读的形式,也是有用的。更好的方法是尽量让参数或返回值本身就足够清楚

  • 警示作用的注释

    func NewExample(accountNode *task.ImTreeNode) *example {
    	// 初始化各类数据
    	// 使用map的时候需要注意并发安全问题
    	return &example{
    		accountNode:  accountNode,
    		friends:      make(map[string]*im.ImUserInfo),
    		groups:       make(map[string]*im.TroopGroupInfo),
    		groupMembers: make(map[string]map[string]*im.ImUserInfo),
    
    		randMath: rand.New(rand.NewSource(time.Now().UnixNano())),
    	}
    }
    
  • TODO注释

    • TODO是一种程序员认为应该做,但由于某些原因还没有做的工作。
    • 但一定要定期查看,删除不再需要的
    case im.ACCOUNT_INFO:
    	s.AccountInfo.UserInfo.ImUserInfo = *val
    	// 不提交头像信息,改字段过大可能会溢出
    	// TODO:优化头像字段,改为编码base64会有一定改善
    	s.AccountInfo.UserInfo.ImUserInfo.Avatar = nil
    	// 账号使用情况统计
    	s.accountSummary.AccountId = val.GetId()
    	s.accountSummary.NickName = val.NickName
    
  • 放大某种看起来不合理之物的重要性的注释。

    // cache 结构体用于缓存数据
    type cache struct {
    	data map[string]string
    	// lastUpdated 用于跟踪缓存最后更新时间
    	lastUpdated time.Time
    }
    

5、什么是坏注释

  • 坏的注释都是糟糕代码的支撑或借口,或者是对错误决策的修正,基本上等于程序员自说自话。

  • 如果你决定要写注释,就要花必要的时间确保写出最好的注释。常问自己写注释的目的是什么?是否可以通过代码避免写这些注释?

  • 避免注释多余。

    // 计算矩形的面积
    func getRectangleArea(width, height float64) float64 {
        // 计算面积并返回
        return width * height
    }
    
  • 不要出现误导性的注释。

  • 日志性注释,我们常常在开发过程中写一些理思路的注释,完成代码后请删除它们。

    //1、获取任务状态
    //2、修改任务..
    //3、...
    //4、更新检材
    
  • 废话注释,用整理代码的决心替代创造废话的冲动吧,这样你就会发现自己将成为更优秀、更快乐的程序员。

  • 标记位置的注释。

    //反面典型
    
    //火眼适配----------------------------
    
  • 写注释不要写你的名字。

    /*
    IsFileExist 判断文件或路径是否存在
    
    author: 张三
    */
    
  • 注释掉的代码,请在迭代测试后删除它们。

  • 非本地信息的注释,写注释要写对地方,确保距离代码最近。

  • 信息过多的注释,别在注释中添加有趣的历史性话题或者无关的细节描述。

格式 #

格式化不仅仅是美观问题,它直接影响代码的可读性。良好的格式可以帮助开发者更快理解代码的意图和逻辑。

垂直格式 #

1、文件应该短,一般以500行代码为最大标准。

2、源文件要像报纸文章那样。名称应当简单一目了然,名称本身应该足以告诉我们是否在正确的模块中。细节应该往下逐次展开,直到找到源文件中最底层的函数和细节。

3、建议代码每行展现一个表达式或者子句,每行代码展示一条完整的思路。这些思路用空白行隔开。(这个编辑器不会帮你)

4、在变量、接口、结构体等之间,都空一行。

5、把存在相互关系的函数、变量、结构图等放在一块。存在紧密关系的代码应该相互靠近。–易于阅读。

常习惯将变量、结构体放到文件开头,这是不对的。

注意:多个函数、文件使用的变量、结构体等放到文件最上方。

6、函数也一样,当你从一个函数A跳转到一个函数B;那么这个函数B应该在函数A的下面最靠近的位置。

7、循环中用到的控制变量应该总是在循环语句中声明。

横向格式 #

1、一行代码应该有多宽?(书作者个人上限是120字符)

2、水平方向上的区隔与靠近、水平对齐、缩进等不用多说。

一个团队应该保持同一种代码风格。 #

对象和数据结构 #

数据抽象 #

1、代码要隐藏数据的具体实现,只暴露必要的操作接口。这样可以减少代码的耦合性,提高代码的可维护性。

2、我们不愿意暴露数据细节,而更愿意以抽象形态表述数据。这并不意味着只是用接口或赋值器、取值器就万事大吉。要以最好的方式呈现某个对象包含的数据,需要进行严肃的思考。

在go语言中,不要乱用开头大写的变量、数据结构在整个系统中乱窜,要封装成函数或方法。

得墨忒耳律 #

1、得墨忒耳律的主要规则是:

  • 类C的方法f只应该调用以下对象的方法:
    1. C。
    2. 由f创建的对象。
    3. 作为参数传递给f的对象。
    4. 由C的尸体变量持有的对象。

在go语言中,一个结构体的方法不应该过多的访问其他结构体的内部属性,而应该通过接口或者方法进行交互。

得墨忒耳律的主要规则是:

  • 结构体(C)的方法只应调用以下对象的方法:
    1. 当前结构体(C)。
    2. 当前方法(f)创建的结构体。
    3. 作为参数传递给当前方法(f)的结构体。
    4. 当前结构体的字段(成员变量)持有的结构体(嵌套)。

2、代码应避免造成火车失事,就是一连串的去调用。

type Engine struct {
    Horsepower int
}

type Car struct {
    Engine Engine
}

type Driver struct {
    Car Car
}

fmt.Println("Horsepower:", driver.Car.Engine.Horsepower)//要通过方法去封装

错误处理 #

// 内置的error接口,是一个常规的用于处理错误的接口。
// 其中 nil 表示没有错误。
type error interface {
    Error() string
}

实现一个错误处理,所需要的只是给 Error () 方法返回一个简单的字符串。

不要重复进行错误处理 #

//反面例子
	err = service.Task.CreateTask(task)
	if err != nil {
		common.Log.Error(err.Error())
		return
	}

func (ts *TaskService) CreateTask(task *vmodel.Task) (err error) {
	tasks, err := proxy.Task.GetTasks(&vmodel.Task{CaseId: task.CaseId, Name: "videothumbnail"}) //获取任务
	if err != nil {
        common.Log.Error(err.Error())
		return err
	}
	......
}

当错误返回时,再次记录错误,然后在系统日志中会发生噩梦般的错误记录。可以这样处理:

func (ta *TaskApi) CreateTask(c *gin.Context, task *vmodel.Task) {
    err := service.Task.CreateTask(task)
	if err != nil {
        common.Log.Error(err.Error())
		response.Fail(c, global.CurdCreatTaskMsg, err.Error())
		return
	}
	.......
	response.Success(c, global.CurdStatusOkMsg, task)
}	

func (ts *TaskService) CreateTask(task *vmodel.Task) (err error) {
	tasks, err := proxy.Task.GetTasks(&vmodel.Task{CaseId: task.CaseId, Name: "videothumbnail"}) //获取任务
	if err != nil {
		return errors.Wrapf(err, "获取任务失败:CaseId:%d,Name:%s",task.CaseId,"videothumbnail")
	}
	......
}

边界 #

边界是指我们的代码与第三方代码库或模块之间的接口。管理这些边界是为了确保第三方代码的变化不会直接影响我们的代码,保持我们的代码的稳定性和可维护性。

1、对于第三方库,应该对其进行封装,提供稳定的接口,使得第三方库的变化不会直接影响我们的代码。

2、限制第三方库的使用范围,尽量在少量的地方使用第三方库,以减少其对整体系统的影响。

学习性测试 #

设想我们对第三方库的使用方法并不清楚,我们可能会花一两天的时间去阅读文档,决定如何使用。然后,我们会编写使用第三方库的代码,看看是否如我们所愿,我们会陷入很长时间的调试,找出我们或他们代码中的缺陷……

编写单元测试以验证我们对第三方库的使用是否正确,同时确保我们的代码不会受到第三方库变化的影响。这种学习性测试毫无成本,可以帮助我们对第三方库的理解。当第三方库发布了新版本,我们可以运行学习性测试看,看看第三方库的行为有没有发生改变。

整洁边界 #

如果有良好的软件设计,则无需巨大投入和重写即可进行修改。在使用我们控制不了的代码时,必须加倍小心保护投资,确保未来的修改不至于太大代价。

边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中的特定信息。依靠你能控制得东西,好过依靠你控制不了的东西,免得日后受他控制

单元测试 #

TDD三定律 #

TDD要求我们在编写生产代码之前先编写单元测试。

  • 在编写不能通过的单元测试之前,不可编写生产代码。
  • 只可编写刚好无法通过的单元测试,不能编译也不能算通过。
  • 只可编写刚好足以通过当前失败测试的生产代码。

保持测试整洁 #

测试代码和生产代码一样重要。他需要被思考、被设计和被照料,他应该像生产代码一样保持整洁。

单元测试可以让你的代码可扩展、可维护、可复用。

**整洁测试的三个要素:**可读性,可读性,可读性。重要的话强调三遍,如何做到可读性?和其他代码一样:明确,简洁,有足够的表达力。

FIRST #

整洁的测试需遵循以下规则

  • 快速,测试应该足够快。
  • 独立,测试应该相互独立。
  • 可重复,测试应该可以在任何环境中重复使用。
  • 自足验证,测试应该有布尔值输出。
  • 及时,测试应该及时编写。