使用Golang实现一套流程可配置,适用于广告、推荐系统的业务性框架——简单应用

在诸如广告、推荐等系统中,我们往往会涉及过滤、召回和排序等过程。随着系统业务变得复杂,代码的耦合和交错会让项目跌入难以维护的深渊。于是模块化设计是复杂系统的必备基础。这篇文章介绍的业务框架脱胎于线上多人协作开发、高并发的竞价广告系统,在实践中不停被优化,直到易于理解和应用。

基础组件

Handler

在系统中,我们定义一个独立的业务逻辑为一个Handler。
比如过滤“机型”信息的逻辑可以叫做DeviceFilterHandler,排序的逻辑叫SortHandler。
Handler的实现也很简单,只要实现frame.HandlerBaseInterface接口和它对应的方法即可(见github):

package mainimport ("fmt""ghgroups/frame"ghgroupscontext "ghgroups/frame/ghgroups_context""reflect"
)type ExampleHandler struct {frame.HandlerBaseInterface
}func NewExampleHandler() *ExampleHandler {return &ExampleHandler{}
}// ///
// ConcreteInterface
func (e *ExampleHandler) Name() string {return reflect.TypeOf(*e).Name()
}// ///
// HandlerBaseInterface
func (e *ExampleHandler) Handle(*ghgroupscontext.GhGroupsContext) bool {fmt.Printf("run %s", e.Name())return true
}

ConcreteInterface

在系统中,我们要求每个组件都要有名字。这样我们可以在配置文件中指定它在流程中的具体位置。
组件通过继承接口ConcreteInterface,并实现其Name方法来暴露自己的名称。它可以被写死(如上例),也可以通过配置文件来指定(见后续案例)。

type ConcreteInterface interface {Name() string
}

HandlerBaseInterface

处理业务逻辑的代码需要在Handle(context *ghgroupscontext.GhGroupsContext) bool中实现的。上例中,我们只让其输出一行文本。
这个方法来源于HandlerBaseInterface接口。

type HandlerBaseInterface interface {ConcreteInterfaceHandle(context *ghgroupscontext.GhGroupsContext) bool
}

因为HandlerBaseInterface 继承自ConcreteInterface ,所以我们只要让自己构建的Handler继承自HandlerBaseInterface,并实现相应方法即可。

应用

一般一个复杂的业务不能只有一个Handler,但是为了便于方便讲解,我们看下怎么运行只有一个Handler的框架(见github)。

package mainimport ("fmt""ghgroups/frame""ghgroups/frame/constructor"ghgroupscontext "ghgroups/frame/ghgroups_context""ghgroups/frame/utils""reflect"
)func main() {constructor := utils.BuildConstructor("")constructor.Register(reflect.TypeOf(ExampleHandler{}))mainProcess := reflect.TypeOf(ExampleHandler{}).Name()run(constructor, mainProcess)
}func run(constructor *constructor.Constructor, mainProcess string) {if err := constructor.CreateConcrete(mainProcess); err != nil {fmt.Printf("%v", err)}if someInterfaced, err := constructor.GetConcrete(mainProcess); err != nil {fmt.Printf("%v", err)} else {if mainHandlerGroup, ok := someInterfaced.(frame.HandlerBaseInterface); !ok {fmt.Printf("mainHandlerGroup %s is not frame.HandlerBaseInterface", mainProcess)} else {context := ghgroupscontext.NewGhGroupsContext(nil)// context.ShowDuration = truemainHandlerGroup.Handle(context)}}
}

在main函数中,我们需要向对象构建器constructor注册我们写的Handler。然后调用run方法,传入构建器和需要启动的组件名(mainProcess)即可。运行结果如下

ExampleHandler
run ExampleHandler

第一行是框架打印的流程图(目前只有一个),第二行是运行时ExampleHandler的Handle方法的执行结果。

HandlerGroup

HandlerGroup是一组串行执行的Handler。
在这里插入图片描述

框架底层已经实现好了HandlerGroup的代码,我们只要把每个Handler实现即可(Handler的代码可以前面的例子)。
然后在配置文件中,配置好Handler的执行顺序:

name: handler_group_a
type: HandlerGroup
handlers: - ExampleAHandler- ExampleBHandler

应用

部分代码如下(完整见github)

package mainimport ("fmt""ghgroups/frame""ghgroups/frame/constructor"constructorbuilder "ghgroups/frame/constructor_builder""ghgroups/frame/factory"ghgroupscontext "ghgroups/frame/ghgroups_context""os""path""reflect"
)func main() {factory := factory.NewFactory()factory.Register(reflect.TypeOf(ExampleAHandler{}))factory.Register(reflect.TypeOf(ExampleBHandler{}))runPath, errGetWd := os.Getwd()if errGetWd != nil {fmt.Printf("%v", errGetWd)return}concretePath := path.Join(runPath, "conf")constructor := constructorbuilder.BuildConstructor(factory, concretePath)mainProcess := "handler_group_a"run(constructor, mainProcess)
}

这次对象构建器我们需要使用constructorbuilder.BuildConstructor去构建。因为其底层会通过配置文件所在的文件夹路径(concretePath )构建所有的组件。而在此之前,需要告诉构建器还有两个我们自定义的组件(ExampleAHandler和ExampleBHandler)需要注册到系统中。于是我们暴露出对象工厂(factory )用于提前注册。
运行结果如下

handler_group_aExampleAHandlerExampleBHandler
run ExampleAHandler
run ExampleBHandler

前三行是配置文件描述的执行流程。后两行是实际执行流程中的输出。

AsyncHandlerGroup

AsyncHandlerGroup是一组并行执行的Handler。
在这里插入图片描述
和HandlerGroup一样,框架已经实现了AsyncHandlerGroup的底层代码,我们只用实现各个Handler即可。
有别于HandlerGroup,它需要将配置文件中的type设置为AsyncHandlerGroup。

name: async_handler_group_a
type: AsyncHandlerGroup
handlers: - ExampleAHandler- ExampleBHandler

应用

使用的代码和HandlerGroup类似,具体见github。
执行结果如下

async_handler_group_aExampleAHandlerExampleBHandler
run ExampleBHandler
run ExampleAHandler

Layer

Layer由两部分组成:Divider和Handler。Handler是一组业务逻辑,Divider用于选择执行哪个Handler。
在这里插入图片描述

Divider

不同于Handler,Divider需要继承和实现DividerBaseInterface接口。

type DividerBaseInterface interface {ConcreteInterfaceSelect(context *ghgroupscontext.GhGroupsContext) string
}

Select方法用于填充业务逻辑,选择该Layer需要执行的Handler的名称。下面是Divider具体实现的一个样例(见github)。

package mainimport ("ghgroups/frame"ghgroupscontext "ghgroups/frame/ghgroups_context""reflect"
)type ExampleDivider struct {frame.DividerBaseInterface
}func NewExampleDivider() *ExampleDivider {return &ExampleDivider{}
}// ///
// ConcreteInterface
func (s *ExampleDivider) Name() string {return reflect.TypeOf(*s).Name()
}// ///
// DividerBaseInterface
func (s *ExampleDivider) Select(context *ghgroupscontext.GhGroupsContext) string {return "ExampleBHandler"
}

应用

每个Layer都要通过配置文件来描述其组成。相较于HandlerGroup,由于它不会执行所有的Handler,而是要通过Divider来选择执行哪个Handler,于是主要是新增Divider的配置项。

name: layer_a
type: Layer
divider: ExampleDivider
handlers: - ExampleAHandler- ExampleBHandler

具体执行的代码见github。我们看下运行结果

layer_aExampleDividerExampleAHandlerExampleBHandler
run ExampleBHandler

可以看到它只是执行了Divider选择的ExampleBHandler。

LayerCenter

LayerCenter是一组串行执行的Layer的组合。
在这里插入图片描述
在使用LayerCenter时,我们只要实现好每个Layer,然后通过配置文件配置它们的关系即可。

type: LayerCenter
name: layer_center
layers: - layer_a- layer_b

应用

具体代码和前面类似,可以见github。
运行结果如下

layer_centerlayer_aExampleADividerExampleA1HandlerExampleA2Handlerlayer_bExampleBDividerExampleB1HandlerExampleB2Handler
run ExampleA2Handler
run ExampleB1Handler

可以看到每个Layer选择了一个Handler执行。

源码见github。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/22753.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android kernel移植5-RK3568

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言1.添加开发板默认配置文件前言 前面我们已经学会了移植uboot,其实就是把瑞芯微的关于uboot的一些文件的名字和编译指定的文件改为自己定义的问价和名字,那么接下来的Android kernel其实也是…

C语言笔记6

关于microsoft visual 的学习笔记 CtrlF5就是启动编译程序 先CtrlA进行全选&#xff0c;然后AitF8就自动的调节代码的格式 #include <stdio.h> #include <stdlib.h> int main() {//system启动程序(在一个程序中启动另外一个程序)//如果程序环境变量中找不到程序&am…

webpack源码简版

Compiler.js:负责整个编译过程的控制和管理&#xff0c;包含compiler构造函数&#xff0c;在构造函数中获取webpack.config.js的出口、入口、参数等Bundler.js:引入webpack.config.js和compiler.js&#xff0c;创建compiler实例化对象并传入options。拿到webpack配置参数后开始…

Java自定义校验注解实现List、set集合字段唯一性校验

文章目录 一&#xff1a; 使用场景二&#xff1a; 定义FieldUniqueValid注解2.1 FieldUniqueValid2.2 注解说明2.3 Constraint 注解介绍2.4 FieldUniqueValid注解使用 三&#xff1a;自定义FieldUniqueValidator校验类3.1 实现ConstraintValidator3.2 重写initialize方法3.3 重…

STM32(HAL)多串口进行重定向(printf函数发送数据)

目录 1、简介 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 串口外设配置 2.3 项目生成 3、KEIL端程序整合 4、效果测试 1、简介 在HAL库中&#xff0c;常用的printf函数是无法使用的。本文通过重映射实现在HAL库多个串口可进行类似printf函数的操作。 2.1 基础配置 2.…

【c++】VSCode配置 c++ 环境(重新制作)

上一篇帖子【c】VSCode配置 c 环境&#xff08;小白教程&#xff09;_vscode配置c/c环境_StudyWinter的博客-CSDN博客 大火&#xff0c;但是依旧有很多小伙伴反应没有配好环境&#xff0c;今天打算重新写一个教程&#xff0c;希望对大家有帮助。 1 MinGW下载安装 在CSDN上传了…

实现邮箱管理之gmail邮箱、office365(Azure)邮箱之披荆斩棘问题一览

要进行Office365邮箱的授权对接&#xff0c;你需要先申请一个应用&#xff0c;并获取授权访问令牌。 以下是一个简单的步骤&#xff1a; 登录 Azure 门户&#xff1a;https://portal.azure.com/创建一个新的应用程序&#xff0c;或者使用现有的应用程序。要创建新的应用程序&…

React Native从文本内容尾部截取显示省略号

<Textstyle{styles.mMeNickname}ellipsizeMode"tail"numberOfLines{1}>{userInfo.nickname}</Text> 参考链接&#xff1a; https://www.reactnative.cn/docs/text#ellipsizemode https://chat.xutongbao.top/

【ASP.NET MVC】使用动软(一)(9)

一、解决的问题 前文为解决数据库操作设计的 TestMysql 类&#xff0c;仅简单地封装了一个Query函数&#xff0c;代码如下&#xff1a; public class TestMysql{public static string SqlserverConnectStr "server127.0.0.1;charsetutf8;user idroot;persistsecurityin…

Redis 在电商秒杀场景中的应用

Redis 在电商秒杀场景中的应用 一、简介1.1 简介1.2 场景应用 二、Redis 优势与挑战2.1 优势2.2 秒杀场景的挑战 三、应用场景分析3.1 库存预热代码示例 3.2 分布式锁3.3 消息队列 四、系统设计方案4.1 架构设计4.2 技术选型4.3 数据结构设计 五、Redis 性能优化5.1 集群部署5.…

数据库数据恢复-Oracle数据库文件出现坏块的数据恢复案例

Oracle数据库故障&初检&分析&#xff1a; 打开Oracle数据库时报错&#xff0c;报错信息&#xff1a;“system01.dbf需要更多的恢复来保持一致性&#xff0c;数据库无法打开”。用户急需恢复zxfg用户下的数据。 出现上述报错的可能原因包括&#xff1a;控制文件损坏、数…

C高级--day3(shell中的输入、命令置换符、数组、算数运算、分支结构)

#!/bin/bash pls ~/ -l | grep "^-" | wc -l qls ~/ -l | grep "^d" | wc -l echo "普通文件个数&#xff1a;$p" echo "目录文件个数&#xff1a;$q"#!/bin/bash read file posexpr index $file \. strexpr substr $file $((pos1)) 2…

什么是Java中的Maven?

Java中的Maven&#xff0c;可以简单理解为“一个神奇的工具”&#xff0c;它可以自动帮你管理Java项目的依赖关系&#xff0c;让你不再为手动下载、配置各种库而烦恼。想象一下&#xff0c;你正在写一个Java项目&#xff0c;突然发现需要引入一个名为"第三方库"的模块…

视频是如何做成gif动图的?1分钟快速转gif动画

常见的电影、电视剧等视频体积较大不易于传输和保存。为了方便大家使用可以将视频制作成GIF&#xff0c;可直接发送给对方非常的方便。那么&#xff0c;要怎么将视频转换成gif动画呢&#xff1f;很简单&#xff0c;使用专业的gif图片在线制作工具–GIF中文网&#xff0c;无需下…

企业电子招投标采购系统java spring cloud+spring boot功能模块功能描述+数字化采购管理 采购招投标

​功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为外…

如何加深Java理解的思考--20230805

目录 一、Java的深度理解二、计算机科学基础三、知识系统结构化和基础化四、学习大牛的开发实践经验与思维逻辑&#xff0c;了解前沿新技术栈、新的解决方案&#xff0c;并批判性地吸收消化。注意思维逻辑很重要&#xff0c;很重要&#xff0c;很重要。很多时候这个东西需要自己…

Git 用户名邮箱的全局配置和单仓库配置(不同项目使用不同账号登录)

Git 用户名邮箱的全局配置和单仓库配置(不同项目使用不同账号登录) 需求 因工作和个人的仓库地址、用户名和邮箱都不一样,很多时候一个git账号无法满足工作和个人学习并行的需求。 全局用户名和邮箱是本地 git 客户端的变量&#xff0c;可配置&#xff0c;不随 git 库而改变…

【IDEA】常用插件清单

【IDEA】常用插件清单 arthas ideaCodeium: AI Autocomplete for xxxCommit-MessageGenerateAllSetterMaven HelperMybatisPlusOne Dark themePDF ViewerRainbow BracketsRestfulToolSequenceDiagramSonarLintTranslation arthas idea 快捷生成arthas命令 Codeium: AI Autoc…

微信云托管(本地调试)⑥:nginx、vue刷新404问题

一、nginx默认路径 1.1、默认配置文件路径&#xff1a;/etc/nginx/nginx.conf 1.2、默认资源路径&#xff1a;/usr/share/nginx/html/index.html 二、修改nginx.conf配置 &#xff08;注意配置中的&#xff1a;include /etc/nginx/conf.d/*.conf; 里面包了一个server配置文件…

大数据面试题:HBase的读写缓存

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 参考答案&#xff1a; HBase上RegionServer的cache主要分为两个部分&#xff1a;MemStore & BlockCache。 MemStore是写缓存&#xff0c;Block…