如何使用高阶函数编程提升代码的简洁性

白银

解决这种问题,一种比较常见的方案是,新建一个结构体,把各种查询的字段都放在这个结构体中,然后把这个结构体作为入参传入到 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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家

这里笔者分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

【视频教程】

天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

了一些资料,需要的可以免费分享给大家

这里笔者分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

[外链图片转存中…(img-5cTd4QDu-1713544303675)]

[外链图片转存中…(img-27xhJIwh-1713544303676)]

【视频教程】

[外链图片转存中…(img-khLGsy8W-1713544303677)]

天道酬勤,只要你想,大厂offer并不是遥不可及!希望本篇文章能为你带来帮助,如果有问题,请在评论区留言。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值