一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - 編程技術 - 如何使用高階函數編程提升代碼的簡潔性

如何使用高階函數編程提升代碼的簡潔性

2022-02-18 00:42字節跳動技術團隊直播運營平臺 編程技術

函數是 Go 語言的一等公民,本文采用一種高階函數的方式,抽象了使用 gorm 查詢 DB 的查詢條件,將多個表的各種復雜的組合查詢抽象成了一個統一的方法和一個配置類,提升了代碼的簡潔和優雅,同時可以提升開發人員的效率。

摘要

函數是 Go 語言的一等公民,本文采用一種高階函數的方式,抽象了使用 gorm 查詢 DB 的查詢條件,將多個表的各種復雜的組合查詢抽象成了一個統一的方法和一個配置類,提升了代碼的簡潔和優雅,同時可以提升開發人員的效率。

背景

有一張 DB 表,業務上需要按照這個表里的不同字段做篩選查詢,這是一個非常普遍的需求,我相信這種需求對于每個做業務開發的人都是繞不開的。比如我們有一張存儲用戶信息的表,簡化之后的表結構如下:

CREATE TABLE `user_info` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `user_id` bigint NOT NULL COMMENT '用戶id', `user_name` varchar NOT NULL COMMENT '用戶姓名', `role` int NOT NULL DEFAULT '0' COMMENT '角色', `status` int NOT NULL DEFAULT '0' COMMENT '狀態', PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶信息表';

這個表里有幾個關鍵字段,user_id、user_name 、 role、status。如果我們想按照 user_id 來做篩選,那我們一般是在 dao 層寫一個這樣的方法(為了示例代碼的簡潔,這里所有示例代碼都省去了錯誤處理部分):

func GetUserInfoByUid(ctx context.Context, userID int64) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo db = db.Where("user_id = ?", userID) db.Find(&infos) return infos }

如果業務上又需要按照 user_name 來查詢,那我們就需要再寫一個類似的方法按照 user_name 來查詢:

func GetUserInfoByName(ctx context.Context, name string) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo db = db.Where("user_name = ?", name) db.Find(&infos) return infos }

可以看到,兩個方法的代碼極度相似,如果再需要按照 role 或者 status 查詢,那不得不再來幾個方法,導致相似的方法非常多。當然很容易想到,我們可以用一個方法,多幾個入參的形式來解決這個問題,于是,我們把上面兩個方法合并成下面這種方法,能夠支持按照多個字段篩選查詢:

func GetUserInfo(ctx context.Context, userID int64, name string, role int, status int) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo if userID > 0 { db = db.Where("user_id = ?", userID)
   } if name != "" { db = db.Where("user_name = ?", name)
   } if role > 0 { db = db.Where("role = ?", role)
   } if status > 0 { db = db.Where("status = ?", status)
   } db.Find(&infos) return infos }

相應地,調用該方法的代碼也需要做出改變:

//只根據UserID查詢 infos := GetUserInfo(ctx, userID, "", 0, 0) //只根據UserName查詢 infos := GetUserInfo(ctx, 0, name, 0, 0) //只根據Role查詢 infos := GetUserInfo(ctx, 0, "", role, 0) //只根據Status查詢 infos := GetUserInfo(ctx, 0, "", 0, status)

這種代碼無論是寫代碼的人還是讀代碼的人,都會感覺非常難受。我們這里只列舉了四個參數,可以想想這個表里如果有十幾個到二十個字段都需要做篩選查詢,這種代碼看上去是一種什么樣的感覺。首先,GetUserInfo 方法本身入參非常多,里面充斥著各種 != 0 和 != ""的判斷,并且需要注意的是,0 一定不能作為字段的有效值,否則 != 0 這種判斷就會有問題。其次,作為調用方,明明只是根據一個字段篩選查詢,卻不得不為其他參數填充一個 0 或者""來占位,而且調用者要特別謹慎,因為一不小心,就可能會把 role 填到了 status 的位置上去,因為他們的類型都一樣,編譯器不會檢查出任何錯誤,很容易搞出業務 bug。

解決方案

如果說解決這種問題有段位,那么以上的寫法只能算是青銅,接下來我們看看白銀、黃金和王者。

白銀

解決這種問題,一種比較常見的方案是,新建一個結構體,把各種查詢的字段都放在這個結構體中,然后把這個結構體作為入參傳入到 dao 層的查詢方法中。而在調用 dao 方法的地方,根據各自的需要,構建包含不同字段的結構體。在這個例子中,我們可以構建一個 UserInfo 的結構體如下:

type UserInfo struct { UserID int64 Name string Role int32 Status int32 }

把 UserInfo 作為入參傳給 GetUserInfo 方法,于是 GetUserInfo 方法變成了這樣:

func GetUserInfo(ctx context.Context, info *UserInfo) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo if info.UserID > 0 { db = db.Where("user_id = ?", info.UserID)
   } if info.Name != "" { db = db.Where("user_name = ?", info.Name)
   } if info.Role > 0 { db = db.Where("role = ?", info.Role)
   } if info.Status > 0 { db = db.Where("status = ?", info.Status)
   } db.Find(&infos) return infos }

相應地,調用該方法的代碼也需要變動:

//只根據userD查詢 info := &UserInfo{ UserID: userID,
} infos := GetUserInfo(ctx, info) //只根據name查詢 info := &UserInfo{ Name: name,
} infos := GetUserInfo(ctx, info)

這個代碼寫到這里,相比最開始的方法其實已經好了不少,至少 dao 層的方法從很多個入參變成了一個,調用方的代碼也可以根據自己的需要構建參數,不需要很多空占位符。但是存在的問題也比較明顯:仍然有很多判空不說,還引入了一個多余的結構體。如果我們就到此結束的話,多少有點遺憾。

另外,如果我們再擴展一下業務場景,我們使用的不是等值查詢,而是多值查詢或者區間查詢,比如查詢 status in (a, b),那上面的代碼又怎么擴展呢?是不是又要引入一個方法,方法繁瑣暫且不說,方法名叫啥都會讓我們糾結很久;或許可以嘗試把每個參數都從單值擴展成數組,然后賦值的地方從 = 改為 in()的方式,所有參數查詢都使用 in 顯然對性能不是那么友好。

黃金

接下來我們看看黃金的解法。在上面的方法中,我們引入了一個多余的結構體,并且無法避免在 dao 層的方法中做了很多判空賦值。那么我們能不能不引入 UserInfo 這個多余的結構體,并且也避免這些丑陋的判空?答案是可以的,函數式編程可以很好地解決這個問題,首先我們需要定義一個函數類型:

type Option func(*gorm.DB)

定義 Option 是一個函數,這個函數的入參類型是*gorm.DB,返回值為空。

然后針對 DB 表中每個需要篩選查詢的字段定義一個函數,為這個字段賦值,像下面這樣:

func UserID(userID int64) Option { return func(db *gorm.DB) { db.Where("`user_id` = ?", userID)
   }
} func UserName(name string) Option { return func(db *gorm.DB) { db.Where("`user_name` = ?", name)
   }
} func Role(role int32) Option { return func(db *gorm.DB) { db.Where("`role` = ?", role)
   }
} func Status(status int32) Option { return func(db *gorm.DB) { db.Where("`status` = ?", status)
   }
}

上面這組代碼中,入參是一個字段的篩選值,返回的是一個 Option 函數,而這個函數的功能是把入參賦值給當前的【db *gorm.DB】對象。這也就是我們在文章一開始就提到的高階函數,跟我們普通的函數不太一樣,普通的函數返回的是一個簡單類型的值或者一個封裝類型的結構體,而這種高階函數返回的是一個具備某種功能的函數。這里多說一句,雖然 go 語言很好地支持了函數式編程,但是由于其目前缺少對泛型的支持,導致高階函數編程的使用并沒有給開發者帶來更多的便利,因此在平時業務代碼中寫高階函數還是略為少見。而熟悉 JAVA 的同學都知道,JAVA 中的 Map、Reduce、Filter 等高階函數使用起來非常的舒服。

好,有了這一組函數之后,我們來看看 dao 層的查詢方法怎么寫:

func GetUserInfo(ctx context.Context, options ...func(option *gorm.DB)) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) for _, option := range options { option(db)
   } var infos []*resource.UserInfo db.Find(&infos) return infos }

沒有對比就沒有傷害,通過和最開始的方法比較,可以看到方法的入參由多個不同類型的參數變成了一組相同類型的函數,因此在處理這些參數的時候,也無需一個一個的判空,而是直接使用一個 for 循環就搞定,相比之前已經簡潔了很多。

那么調用該方法的代碼怎么寫呢,這里直接給出來:

//只使用userID查詢 infos := GetUserInfo(ctx, UserID(userID)) //只使用userName查詢 infos := GetUserInfo(ctx, UserName(name)) //使用role和status同時查詢 infos := GetUserInfo(ctx, Role(role), Status(status))

無論是使用任意的單個參數還是使用多個參數組合查詢,我們都隨便寫,不用關注參數順序,簡潔又清晰,可讀性也是非常好。

再來考慮上面提到的擴展場景,如果我們需要多值查詢,比如查詢多個 status,那么我們只需要在 Option 中增加一個小小的函數即可:

func StatusIn(status []int32) Option { return func(db *gorm.DB) { db.Where("`status` in ?", status)
   }
}

對于其他字段或者等值查詢也是同理,代碼的簡潔不言而喻。

王者

能優化到上面黃金的階段,其實已經很簡潔了,如果止步于此的話,也是完全可以的。但是如果還想進一步追求極致,那么請繼續往下看!

在上面方法中,我們通過高階函數已經很好地解決了對于一張表中多字段組合查詢的代碼繁瑣問題,但是對于不同的表查詢,仍然要針對每個表都寫一個查詢方法,那么還有沒有進一步優化的空間呢?我們發現,在 Option 中定義的這一組高階函數,壓根與某張表沒關系,他只是簡單地給 gorm.DB 賦值。因此,如果我們有多張表,每個表里都有 user_id、is_deleted、create_time、update_time 這些公共的字段,那么我們完全不用再重復定義一次,只需要在 Option 中定義一個就夠了,每張表的查詢都可以復用這些函數。進一步思考,我們發現,Option 中維護的是一些傻瓜式的代碼,根本不需要我們每次手動去寫,可以使用腳本生成,掃描一遍 DB 的表,為每個不重復的字段生成一個 Equal 方法、In 方法、Greater 方法、Less 方法,就可以解決所有表中按照不同字段做等值查詢、多值查詢、區間查詢。

解決了 Option 的問題之后,對于每個表的各種組合查詢,就只需要寫一個很簡單的 Get 方法了,為了方便看,我們在這里再貼一次:

func GetUserInfo(ctx context.Context, options ...func(option *gorm.DB)) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) for _, option := range options { option(db)
   } var infos []*resource.UserInfo db.Find(&infos) return infos }

上面這個查詢方法是針對 user_info 這個表寫的,如果還有其他表,我們還需要為每個表都寫一個和這個類似的 Get 方法。如果我們仔細觀察每個表的 Get 方法,會發現這些方法其實就有兩點不同:

  • 返回值類型不一樣;
  • TableName 不一樣。

如果我們能解決這兩個問題,那我們就能夠使用一個方法解決所有表的查詢。首先對于第一點返回值不一致的問題,可以參考 json.unmarshal 的做法,把返回類型以一個參數的形式傳進來,因為傳入的是指針類型,所以就不用再給返回值了;而對于 tableName 不一致的問題,其實可以和上面處理不同參數的方式一樣,增加一個 Option 方法來解決:

func TableName(tableName string) Option { return func(db *gorm.DB) { db.Table(tableName)
   }
}

這樣改造之后,我們的 dao 層查詢方法就變成了這樣:

func GetRecord(ctx context.Context, in interface{}, options ...func(option *gorm.DB)) { db := GetDB(ctx) for _, option := range options { option(db)
   } db.Find(in) return }

注意,我們把方法名從之前的 GetUserInfo 變成了GetRecord,因為這個方法不僅能支持對于 user_info 表的查詢,而且能夠支持對一個庫中所有表的查詢。也就是說從最開始為每個表建一個類,每個類下面又寫很多個查詢方法,現在變成了所有表所有查詢適用一個方法。

然后我們看看調用這個方法的代碼怎么寫:

//根據userID和userName查詢 var infos []*resource.UserInfo GetRecord(ctx, &infos, TableName(resource.UserInfo{}.TableName()), UserID(userID), UserName(name))

這里還是給出了查詢 user_info 表的示例,在調用的地方指定 tableName 和返回類型。

經過這樣的改造之后,我們最終實現了用一個簡單的方法【GetRecord】 + 一個可自動生成的配置類【Option】對一個庫中所有表的多種組合查詢。代碼的簡潔和優雅又有了一些提升。美中不足的是,在調用查詢方法的地方多傳了兩個參數,一個是返回值變量,一個是 tableName,多少顯得有點不那么美觀。

總結

這里通過對 grom 查詢條件的抽象,大大簡化了對 DB 組合查詢的寫法,提升了代碼的簡潔。對于其他 update、insert、delete 三種操作,也可以借用這種思想做一定程度的簡化,因為篇幅關系我們不在這里贅述。如果大家還有其他想法,歡迎留言討論!

參考文獻

https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

https://coolshell.cn/articles/21146.html

原文地址:https://mp.weixin.qq.com/s/w1ebAgnzfDzoGG0sn6KGlQ

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 国产成人精视频在线观看免费 | 亚洲上最大成网人站4438 | 美女撒尿无遮挡免费中国 | 好大好硬好紧太深了受不了 | 国产动作大片 | 免看一级a一片成人123 | 美女班主任让我爽了一夜视频 | 四虎1515hhh co m | 我和寂寞孕妇的性事 | 精品午夜中文字幕熟女人妻在线 | rylskyart系列视频 | 成人夜视频寂寞在线观看 | 亚洲精品午夜视频 | 99影视在线视频免费观看 | 美女黄a | 精品国产三级av在线 | 日本大片在线 | 亚洲久草| 国产亚洲精品看片在线观看 | 被强上后我成瘾了小说 | 黑人异族日本人hd | 三上悠亚精品专区久久 | 九九精品免视频国产成人 | 男女真实无遮挡xx00动态图软件 | pregnant欧美孕交xxx | 精品一区二区视频 | 天天综合亚洲 | 好大夫在线个人空间 | 啪啪大幂幂被c | 动漫人物差差差动漫人物免费观看 | 欧美日韩一区二区三区在线播放 | 99热在线精品播放 | 九九精品成人免费国产片 | 亚洲一卡2卡三卡4卡5卡组 | 美女和男人免费网站视频 | 视频免费观看在线播放高清 | 国产成人一区二区三区在线视频 | 好大好硬好深好爽想要之黄蓉 | 欧美日韩在线观看精品 | 糖心在线观看 | 国产在线拍 |