av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

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

摘要

背景

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

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供萬源網(wǎng)站建設(shè)、萬源做網(wǎng)站、萬源網(wǎng)站設(shè)計、萬源網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、萬源企業(yè)網(wǎng)站模板建站服務(wù),10多年萬源做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。

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 '狀態(tài)',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶信息表';

這個表里有幾個關(guān)鍵字段,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
}

如果業(yè)務(wù)上又需要按照 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 查詢,那不得不再來幾個方法,導(dǎo)致相似的方法非常多。當(dāng)然很容易想到,我們可以用一個方法,多幾個入?yún)⒌男问絹斫鉀Q這個問題,于是,我們把上面兩個方法合并成下面這種方法,能夠支持按照多個字段篩選查詢:

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
}

相應(yīng)地,調(diào)用該方法的代碼也需要做出改變:

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

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

解決方案

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

白銀

解決這種問題,一種比較常見的方案是,新建一個結(jié)構(gòu)體,把各種查詢的字段都放在這個結(jié)構(gòu)體中,然后把這個結(jié)構(gòu)體作為入?yún)魅氲?dao 層的查詢方法中。而在調(diào)用 dao 方法的地方,根據(jù)各自的需要,構(gòu)建包含不同字段的結(jié)構(gòu)體。在這個例子中,我們可以構(gòu)建一個 UserInfo 的結(jié)構(gòu)體如下:


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

把 UserInfo 作為入?yún)鹘o 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
}

相應(yīng)地,調(diào)用該方法的代碼也需要變動:

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

這個代碼寫到這里,相比最開始的方法其實已經(jīng)好了不少,至少 dao 層的方法從很多個入?yún)⒆兂闪艘粋€,調(diào)用方的代碼也可以根據(jù)自己的需要構(gòu)建參數(shù),不需要很多空占位符。但是存在的問題也比較明顯:仍然有很多判空不說,還引入了一個多余的結(jié)構(gòu)體。如果我們就到此結(jié)束的話,多少有點遺憾。

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

黃金

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

type Option func(*gorm.DB)

定義 Option 是一個函數(shù),這個函數(shù)的入?yún)㈩愋褪?gorm.DB,返回值為空。

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

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)
}
}

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

好,有了這一組函數(shù)之后,我們來看看 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
}

沒有對比就沒有傷害,通過和最開始的方法比較,可以看到方法的入?yún)⒂啥鄠€不同類型的參數(shù)變成了一組相同類型的函數(shù),因此在處理這些參數(shù)的時候,也無需一個一個的判空,而是直接使用一個 for 循環(huán)就搞定,相比之前已經(jīng)簡潔了很多。

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

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

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

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

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

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

王者

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

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

解決了 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 方法。如果我們仔細(xì)觀察每個表的 Get 方法,會發(fā)現(xiàn)這些方法其實就有兩點不同:

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

如果我們能解決這兩個問題,那我們就能夠使用一個方法解決所有表的查詢。首先對于第一點返回值不一致的問題,可以參考 json.unmarshal 的做法,把返回類型以一個參數(shù)的形式傳進(jìn)來,因為傳入的是指針類型,所以就不用再給返回值了;而對于 tableName 不一致的問題,其實可以和上面處理不同參數(shù)的方式一樣,增加一個 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 表的查詢,而且能夠支持對一個庫中所有表的查詢。也就是說從最開始為每個表建一個類,每個類下面又寫很多個查詢方法,現(xiàn)在變成了所有表所有查詢適用一個方法。

然后我們看看調(diào)用這個方法的代碼怎么寫:

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

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

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

總結(jié)

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

參考文獻(xiàn)

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

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


網(wǎng)頁標(biāo)題:如何使用高階函數(shù)編程提升代碼的簡潔性
當(dāng)前路徑:http://uogjgqi.cn/article/cdceges.html
掃二維碼與項目經(jīng)理溝通

我們在微信上24小時期待你的聲音

解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流