范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

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

  摘要
  函数是 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://job.toutiao.com/s/Lts3xLP
  内推邮箱 :liuzhibing.buaa@bytedance.com

获英特尔Evo认证的荣耀MagicBookV14迎综艺首秀?马东又要干嘛?首先,我们需要了解一下英特尔Evo认证的含金量,为了能够提升笔记本的超能体验,英特尔的工程师们和先进的硬件和软件合作伙伴共同制定了Evo严苛的认证标准,基于笔记本真实的使用环境,当windows哪个系统最流畅?个人认为Windows最流畅的系统是Windows7专业版和Windows10企业版LTSCWindows7专业版包含了Windows7家庭版的所有功能,虽然不如Windows7旗为什么内行人只买6G运存的手机,而不选8G运存,究竟怎么回事?只是一个错误的提问。因为从性能上来讲,安卓手机的系统有一大问题,就是打开的程序会越来越多,占用手机的一级运行缓存的越来越大,同时,由于各APP的不断升级,它们对手机一级运行缓存的开Tableau宣布退出中国市场,中国用户该何去何从?脱钩进入深水区了呗。在中国越来越注重信息安全的今天,发生这种事情就是非常正常的事情了。大家往下看,将来在计算机领域内,就会出现如金灿荣教授口中所说的那个场景,一边是中国的,一边是世我能把芯片和光刻机核心技术完全掌握,华为公司能给我多少年薪?华为年薪高的先不说,国家先找你谈话了。好家伙,成千上百个人都研究不透的东西你给琢磨透彻了,这不得秘密接走安全保护一下。华为先不用去了,先去中科院把研究成果成果汇报一下,验证真伪以后美团关联公司新增快递业务民航局叫停客改货极兔收购百世国内快递业务有关键进展美团关联公司新增快递业务11月22日,天眼查APP显示,辽宁美鲜达快运有限公司发生工商变更,公司经营范围新增快递服务。该公司成立于2021年8月,法定代表人为吕楠,注册资本300万同为科技公司,联想和华为的区别是什么?联想和华为最大的区别我认为有三点,第一点是领导人,第二点是企业方向,第三点就是爱国精神。第一点,领导人。联想在柳传志接手后,不断的在走下坡路。电脑领域占比直线下滑,移动手机也不见起iPhone14Pro曝光变得不仅仅是接口很多用户对iPhone手机前置刘海设计不满意,不过苹果的iPhone13系列手机还是保留了前置刘海设计。根据多方爆料显示苹果明年将要推出的iPhone14系列手机将会正式取消前置刘联想不断妥协,5500mAh大电池骁龙888,16512G降1300联想这一品牌想必大家对于它都并不陌生,该品牌最为出名的便是电脑行业,虽说该品牌在手机业务上面不怎么样,但是它并不想放弃手机这一业务。而在联想手机销量不怎么乐观之后,就把注意力转移到退换货江湖起风抱上电商大腿,顺丰百度为何打出0元牌图IC。快递公司攻城略地,退换货业务成了流量焦虑的突破口。冲动消费凑单拼单拍错未退款剁手狂欢后,网店买家免不了退换货。我一般拿不准买哪个颜色,就都下单,回来试试才知道哪个适合我。深2022年后互联网保险时代个人9大猜想大家好,我是数据君。明年1月起保险市场风云如何变化,以下为数据君个人猜测1独立代理人爆发的一年数据君估测,从明年1月起,互联网保险新规下,约45的保司不能通过互联网销售储蓄险业务,
5G网络专用缩略语介绍3GPPThirdGenerationPartnershipProject第三代移动通信伙伴组织5G5thGeneration第五代移动通信技术5GC5GCorenetwork5G苹果拿下双料冠军,国产手机还有赶超机会吗中国商报(记者赵熠如)根据市场研究机构的数据,2021年第四季度苹果手机销量拿下中国市场和全球市场双第一,对于奋力冲击高端市场的国产品牌而言,赶超苹果还有多少机会值得关注。四季度苹世界各地的人们在谷歌地球里发现的5个有趣小秘密谷歌地图在全球拥有大量的用户,事实上,任何人在任何时间都可以通过它从太空看到陆地上的许多景色,它还向我们展示了关于我们从未去过的地方的复杂细节,例如从整个大陆一直到街景中被遗忘的随苹果要上挖空屏却被喷?网友颜值气质这块还得魅族自从iPhoneX手机发布以来到去年的iPhone13系列,苹果正面的刘海屏设计已经延续了五年。而该刘海最大的升级还仅是上代缩小30面积这一个改变,这若放到单挖孔屏遍地的安卓手机里B站UP主攒了一台全纸电脑显卡CPU惟妙惟肖很多男生喜欢攒机,想打造一台顶配电脑,无奈囊中羞涩怎么办呢?那就用纸攒个吧。近日,B站UP主dream胖虎w就用纸板打造了CPU显卡内存散热器机箱等硬件,组装出了一个全纸电脑。只见24英寸千元美术显示器值得入手吗?亲测体验,没入手的可以看一下各种配置的显示器,不同的需求选择不同,对于图片处理工作者而言,更偏爱专业的美术显示器。INNOCN这个品牌在美术显示器领域上,表现更加优秀出众,成为同行的标杆品牌。最近就帮同事入手拓展工作视野,提升娱乐体验,Innocn便携式触控显示屏体验CiaoBello,我是老房。多屏幕办公对效率的提升大家都明白,但在差旅途中会议现场商务协调的时候,只有一台笔记本电脑,让习惯了多屏协同的我们往往感叹效率被大大拉低。同样的,大屏幕三星生物制剂将收购百健(BIIB。US)所持三星Bioepis股份金额达23亿美元智通财经APP获悉,据一份监管备案文件显示,韩国三星集团旗下三星生物制剂(SamsungBiologics)将以23亿美元从百健(BIIB。US)手中收购三星Bioepis49。9北京建工环境修复公司获2021年度中国科学院科技促进发展奖本报讯日前,2021年度中国科学院科技促进发展奖揭晓,由中国科学院地理科学与资源研究所领衔,建工修复公司作为主要合作单位组成的工业污染场地修复关键技术与应用科研团队荣获该项大奖,标苹果iPhone增长强劲国产安卓厂商欲破局高端市场1月28日,苹果公司发布2022财年第一财季(2021年四季度)财报。苹果第一财季营收为1239。45亿美元,同比增长11,创下新高净利润为346。30亿美元,同比增长20。营收和苹果iOS15。4更新的37个表情中,一个怀孕的男人出现在了iphone上从怀孕的男人到咬着的嘴唇,iphone的37个新表情终于发布了。苹果自己设计的新表情已经在周四发布的iOS15。4软件更新的测试版中可用,该版本将于今年春天正式发布。Emojipe