之前用go语言开发考试系统,最近闲下来决定改一下之前的狗屎代码。注意以下所有代码并非完全真实的源码。
本次代码场景如下:考试系统有多种题型,在后台接口中有导入和查询两个需求,如果新增了题型,就需要手动去多个代码文件中修改查询语句、循环赋值、定义变量。
动态sql
// 之前的版本
// 查询分类下有多少题目、多少单选题目、多选题目...
// categorySelect 查询
type categorySelect struct {
CategoryID uint `json:"category_id"`
Num uint `json:"num"`
SingleChoice uint `json:"single_choice"`
MultipleChoice uint `json:"multiple_choice"`
ShortAnswer uint `json:"short_answer"`
BlankAnswer uint `json:"blank_answer"`
TrueFalse uint `json:"true_false"`
}
// 查询分类下题目数量
var categories []categorySelect
if err := categoryQuery.Model(&question{}).
Select("category_id, count(*) as num, SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) as single_choice, SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) as multiple_choice, SUM(CASE WHEN type = 3 THEN 1 ELSE 0 END) as true_false, SUM(CASE WHEN type = 4 THEN 1 ELSE 0 END) as blank_answer, SUM(CASE WHEN type = 5 THEN 1 ELSE 0 END) as short_answer").
Group("category_id").
Find(&categories).Error; err != nil {
return err
}
按之前的逻辑,每次增加新题型我都需要修改select语句,统计新题型。第一步优化就是使用动态sql拼接。好处很明显,每次修改sql select语句容易出错也很不优雅,还可能因为太多地方用到而忘记修改。
// 首先定义题型map, string对应枚举
var QuestionMap = map[string]uint{
"single_choice": SingleChoice,
"multiple_choice": MultipleChoice,
"true_false": TrueFalse,
"blank_answer": BlankAnswer,
"short_answer": ShortAnswer,
}
// 然后遍历map,拼接sql
typeCountCases := make([]string, 0)
for k, v := range QuestionMap {
typeCountCases = append(typeCountCases, fmt.Sprintf("SUM(CASE WHEN type = %d THEN 1 ELSE 0 END) as %s", v, k))
}
Select("category_id, count(*) as num, " + strings.Join(typeCountCases, ", ")).
优化及进度:20%,提升:20%。
递归统计子分类
这个代码要改的地方还不少,第二个就是改掉统计子分类的代码。因为分类有层级关系,而父分类的题目数量也需要加上所有子分类的和。因为最多只有三级分类,所以之前的代码离谱但能用。
// 依托代码,可以直接跳过
// 递归计算分类下题目数量
var numCategory func(list []QuestionCategory, numList []categorySelect) []categoryList
numCategory = func(list []QuestionCategory, numList []categorySelect) []categoryList {
categories := make([]categoryList, 0)
for _, v := range list {
var category categoryList
category.ID = v.ID
category.Name = v.Name
for _, num := range numList {
if num.CategoryID == v.ID {
category.TotalNum = num.Num
category.QuestionNum = num.Num
category.SingleChoiceNum = num.SingleChoice
category.MultipleChoiceNum = num.MultipleChoice
}
}
if len(v.Child) > 0 {
category.Child = numCategory(v.Child, numList)
}
categories = append(categories, category)
}
return categories
}
final := numCategory(list, categoryArr)
for k, v := range final {
if len(v.Child) > 0 {
for k2, child := range v.Child {
v.TotalNum += child.QuestionNum
v.SingleChoiceNum += child.SingleChoiceNum
v.MultipleChoiceNum += child.MultipleChoiceNum
if len(child.Child) > 0 {
for _, child2 := range child.Child {
v.TotalNum += child2.QuestionNum
v.Child[k2].TotalNum += child2.QuestionNum
v.SingleChoiceNum += child2.SingleChoiceNum
v.MultipleChoiceNum += child2.MultipleChoiceNum
v.Child[k2].SingleChoiceNum += child2.SingleChoiceNum
v.Child[k2].MultipleChoiceNum += child2.MultipleChoiceNum
}
}
}
}
final[k] = v
}
这段代码说实话我看了都脸红,好像用上了递归,然后又在那用父分类+子分类+子子分类。好好好,纯脑浆代码,我觉得有可能是ai帮我写了一部分(ai:这代码我写不出来)
// 优化了一版
func numCategory(userInfo common.UserInfo, list []QuestionCategory, numList []categorySelect) []categoryList {
var categories []categoryList
for _, v := range list {
var category categoryList
category.ID = v.ID
category.Name = v.Name
category.Sort = v.Sort
category.Level = v.Level
category.ParentID = v.ParentID
if len(v.Child) > 0 {
category.Child = numCategory(userInfo, v.Child, numList)
}
category.ChangeAuth = true
if userInfo.RoleID == define.Teacher && v.CreatorID != userInfo.UserID {
category.ChangeAuth = false
}
for _, num := range numList {
if num.CategoryID == category.ID {
category.TotalNum = num.Num
category.SingleChoiceNum = num.SingleChoice
category.MultipleChoiceNum = num.MultipleChoice
break
}
}
for _, child := range category.Child {
category.TotalNum += child.TotalNum
category.SingleChoiceNum += child.SingleChoiceNum
category.MultipleChoiceNum += child.MultipleChoiceNum
}
categories = append(categories, category)
}
return categories
}
说实话能写出之前那种代码,说明我完全不懂的递归怎么写,新版也是我让AI写的,这次必须得给递归拿下了。
进度:50%,提升:60%,恶心程度:100%。
反射赋值
优化完递归的代码已经好些了,但我继续问AI,能不能连定义变量和赋值都省去,AI说可以,跟我想的一样用反射实现。
// FillQuestionTypeNum 使用反射填充 QuestionTypeNum 结构体的字段
func FillQuestionTypeNum(target *QuestionTypeNum, source *QuestionTypeNum) {
if target == nil || source == nil {
return
}
targetValue := reflect.ValueOf(target).Elem()
targetType := reflect.TypeOf(target).Elem()
numValue := reflect.ValueOf(source).Elem()
for i := 0; i < targetValue.NumField(); i++ {
field := targetValue.Field(i)
fieldName := targetType.Field(i).Name
numField := numValue.FieldByName(fieldName)
if field.Kind() == reflect.Uint && numField.IsValid() && numField.Kind() == reflect.Uint {
field.SetUint(numField.Uint())
}
}
}
实现的还可以,就当我有点得意的时候,我突然意识到,这件事为什么要用到反射呢?直接抽象出一个赋值函数不就好了吗?
// 用来查询sql的结构体
type categorySelect struct {
QuestionTypeNum
}
// 返回给前端的结构体
type categoryList struct {
QuestionTypeNum
}
// 定义一个题目类型数量查询的结构体
type QuestionTypeNum struct {
SingleChoiceNum uint
MultipleChoiceNum uint
}
// 定义一个public的赋值函数
func FillQuestionTypeNum(target *categoryList, source *categorySelect) {
if target == nil || source == nil {
return
}
target.SingleChoiceNum = source.SingleChoiceNum
...
}
按照这样的方法,新增题型后,只需要新增QuestionTypeNum结构体,然后在FillQuestionTypeNum中赋值即可。也就比反射多了一步,我觉得这样就挺好了。
进度:80%,提升:100%,恶心程度:50%。
使用map替代switch
switch代码写起来非常好理解,(ai填充的也很好),但还是如果新增了题型就要去修个各个地方的switch判断题目类型的代码,改成map会更方便。
// 以前是以前
var questionType int
switch typeStr {
case "all":
questionType = 0
case "single_choice":
questionType = 1
case "multiple_choice":
questionType = 2
case "true_or_false":
questionType = 3
case "fill_blank":
questionType = 4
case "short_answer":
questionType = 5
default:
continue
}
// 现在是现在
// 之前定义的题目类型map派上用场
var questionType int
val, ok := define.QuestionMap[typeStr]
if !ok {
continue
}
questionType = val
进度:100%,提升:100%。
反思一下
此次优化代码算是圆满完成了,虽然肯定还是有不够合理的地方,但毕竟是后台代码,有点小瑕疵只要结果是正确的就还好。虽然提升确实很大,改完也舒服了,但这还是得益于之前写的太烂了,不知道是太赶了还是太划了,感觉写的既不简洁也不优雅,新增了题型要改动好多地方的代码。
此次的启发:
- 写完的代码重新检查一下有惊喜
- 有些明知道写的不够好的,也懒得去修改,这样真不行
- 代码健壮性是第一位的,无论怎么样应该降低出错的概率
下一步是优化题目类型的设计模式,使用类似继承、组合的方式,尽可能减少在业务代码中大段的switch判断,同时也能保证业务代码的扩展性。