内容简介:在公司内见到无数的人在前仆后继地造规则引擎,起因比较简单,drools 之类的东西是 Java 生态的东西,与 Go 血缘不合,商业规则引擎又大多超重量级,从零开始建设的系统使用起来有很高的学习成本。刚好可能也不是很想写 CRUD,几个人一拍即合,所以就又有了造轮子的师出之名。要造一个规则引擎,说难实际上也不难。程序员们这时候捡起了学生时代的编译原理书,抄起递归下降、 lex/yacc 或者再先进一点的 antlr 之类的 parser generator 就搞了起来。造的时候说不定还发现噢噢,大多数 pa
在公司内见到无数的人在前仆后继地造规则引擎,起因比较简单,drools 之类的东西是 Java 生态的东西,与 Go 血缘不合,商业规则引擎又大多超重量级,从零开始建设的系统使用起来有很高的学习成本。刚好可能也不是很想写 CRUD,几个人一拍即合,所以就又有了造轮子的师出之名。
要造一个规则引擎,说难实际上也不难。程序员们这时候捡起了学生时代的编译原理书,抄起递归下降、 lex/yacc 或者再先进一点的 antlr 之类的 parser generator 就搞了起来。造的时候说不定还发现噢噢,大多数 parser generator 还有不支持左递归的问题,然后按照它支持的文法写出的 parser 需要自己处理计算表达式的左结合问题,嗯,非常有成就感,不知道比 CRUD 高到哪里去了。
不过多久就写出了一个谁也不是很好看懂的新轮子。
实际上要那么费劲吗?显然是不用的。被很多人选择性忽略的事实是,Go 的 parser api 是直接暴露给用户的。可能接下来你已经知道我要说什么了。
对的,你可以直接使用 Go 的内置 parser 库完成上面一个基本规则引擎的框架。从功能上来讲,规则引擎的基本就是一个 bool 表达式的解析和求值过程。bool 表达式是啥呢?很简单:
|--bool 表达式--| if a == 1 && b == 2 { // do your business }
你每天都在写的无聊透顶的 if else 就是各种 bool 表达式啊。你别看他无聊,没有 bool 表达式的话,任何程序都没有办法顺利地组织其逻辑,也就没有什么 control flow 一说了。
先写一个简单的 demo,来 parse 并打印上面代码中的 a == 1 && b == 2
:
package main import ( "fmt" "go/ast" "go/parser" "go/token" ) func main() { expr := `a == 1 && b == 2` fset := token.NewFileSet() exprAst, err := parser.ParseExpr(expr) if err != nil { fmt.Println(err) return } ast.Print(fset, exprAst) }
凑合看看,bool 逻辑一般解析后就是最最简单的 AST:
0 *ast.BinaryExpr { 1 . X: *ast.BinaryExpr { 2 . . X: *ast.Ident { 3 . . . NamePos: - 4 . . . Name: "a" 5 . . . Obj: *ast.Object { 6 . . . . Kind: bad 7 . . . . Name: "" 8 . . . } 9 . . } 10 . . OpPos: - 11 . . Op: == 12 . . Y: *ast.BasicLit { 13 . . . ValuePos: - 14 . . . Kind: INT 15 . . . Value: "1" 16 . . } 17 . } 18 . OpPos: - 19 . Op: && 20 . Y: *ast.BinaryExpr { 21 . . X: *ast.Ident { 22 . . . NamePos: - 23 . . . Name: "b" 24 . . . Obj: *(obj @ 5) 25 . . } 26 . . OpPos: - 27 . . Op: == 28 . . Y: *ast.BasicLit { 29 . . . ValuePos: - 30 . . . Kind: INT 31 . . . Value: "2" 32 . . } 33 . } 34 }
这种 AST 实在太常见了以致于我都不是很想解释。。。大多数存储系统的查询 DSL 部分都会有 bool 表达式的痕迹,比如 Elasticsearch,SQL 语句的 where 等等,两年前我曾经造过一个把 SQL 和 Elasticsearch 的 DSL 互相转换的轮子,当时还写了篇文章讲了讲原理: https://elasticsearch.cn/article/114 。
Elasticsearch 在 7.0 的 xpack 中已经开始渐渐支持 SQL 功能了,所以这个轮子慢慢地也就变成了时代的眼泪。
眼泪归眼泪,这种“逻辑”上的“是”或者“否”的判断表达式,都是可以互相对应的,不管哪类的系统,谁设计的多么丑陋的 DSL,大抵上都是可以通过简单的 (field op value) and/or 连接并且有括号的基本表达式来表达的。为啥还有这么多乱七八糟的 DSL?我想了想,基本的原因有三个:
- 该系统的作者觉得普通的 bool 表达式扩展能力不强,自己造的 DSL 一定更牛逼
- 作者不是很会写基本的 bool 表达式的 parser。。。。
- 单纯的想要造一个轮子。
仔细看看,主观的因素两个,客观的因素是 bool 表达式扩展能力不强。嗯,我们来想想,比较典型的 bool 表达式场景:SQL 的表达能力不强吗?普通需求满足不了时,SQL 是怎么进行扩展的呢?
答案其实也挺简单,SQL 的功能可以通过函数来进行扩展,比如 SQL 里支持 group_concat、date_sub 之类的函数,也支持一些简单的 ETL 功能,比如 from_unixtime,unix_timestamp 等等。这一点,在本文开头提出的使用 Go 内部 parser 来实现的规则引擎中可以支持么?
显然你在 Go 里也写过这种 if 判断里有函数调用的逻辑:
func main() { expr := `a == 1 && b == 2 && in_array(c, []int{1,2,3,4})` fset := token.NewFileSet() exprAst, err := parser.ParseExpr(expr) if err != nil { fmt.Println(err) return } ast.Print(fset, exprAst) }
输出内容:
0 *ast.BinaryExpr { 1 . X: *ast.BinaryExpr { 2 . . X: *ast.BinaryExpr { 3 . . . X: *ast.Ident { 4 . . . . NamePos: - 5 . . . . Name: "a" 6 . . . . Obj: *ast.Object { 7 . . . . . Kind: bad 8 . . . . . Name: "" 9 . . . . } 10 . . . } 11 . . . OpPos: - 12 . . . Op: == 13 . . . Y: *ast.BasicLit { 14 . . . . ValuePos: - 15 . . . . Kind: INT 16 . . . . Value: "1" 17 . . . } 18 . . } 19 . . OpPos: - 20 . . Op: && 21 . . Y: *ast.BinaryExpr { 22 . . . X: *ast.Ident { 23 . . . . NamePos: - 24 . . . . Name: "b" 25 . . . . Obj: *(obj @ 6) 26 . . . } 27 . . . OpPos: - 28 . . . Op: == 29 . . . Y: *ast.BasicLit { 30 . . . . ValuePos: - 31 . . . . Kind: INT 32 . . . . Value: "2" 33 . . . } 34 . . } 35 . } 36 . OpPos: - 37 . Op: && 38 . Y: *ast.CallExpr { 39 . . Fun: *ast.Ident { 40 . . . NamePos: - 41 . . . Name: "in_array" 42 . . . Obj: *(obj @ 6) 43 . . } 44 . . Lparen: - 45 . . Args: []ast.Expr (len = 2) { 46 . . . 0: *ast.Ident { 47 . . . . NamePos: - 48 . . . . Name: "c" 49 . . . . Obj: *(obj @ 6) 50 . . . } 51 . . . 1: *ast.CompositeLit { 52 . . . . Type: *ast.ArrayType { 53 . . . . . Lbrack: - 54 . . . . . Elt: *ast.Ident { 55 . . . . . . NamePos: - 56 . . . . . . Name: "int" 57 . . . . . . Obj: *(obj @ 6) 58 . . . . . } 59 . . . . } 60 . . . . Lbrace: - 61 . . . . Elts: []ast.Expr (len = 4) { 62 . . . . . 0: *ast.BasicLit { 63 . . . . . . ValuePos: - 64 . . . . . . Kind: INT 65 . . . . . . Value: "1" 66 . . . . . } 67 . . . . . 1: *ast.BasicLit { 68 . . . . . . ValuePos: - 69 . . . . . . Kind: INT 70 . . . . . . Value: "2" 71 . . . . . } 72 . . . . . 2: *ast.BasicLit { 73 . . . . . . ValuePos: - 74 . . . . . . Kind: INT 75 . . . . . . Value: "3" 76 . . . . . } 77 . . . . . 3: *ast.BasicLit { 78 . . . . . . ValuePos: - 79 . . . . . . Kind: INT 80 . . . . . . Value: "4" 81 . . . . . } 82 . . . . } 83 . . . . Rbrace: - 84 . . . . Incomplete: false 85 . . . } 86 . . } 87 . . Ellipsis: - 88 . . Rparen: - 89 . } 90 }
有了这些东西,在 parser 层面你要做的事情其实基本也就没啥了。只不过需要简单查查 Go 的语言 spec,看看 expression 到底支持哪些语法。
实在不是不得已,根本没有必要造新的 DSL 和 parser。况且在一套生态里做出另一种奇怪的语言来,你不觉得恶心吗?
当然,说归说,业务系统中的 DSL 这种东西一般是给 程序员 来用的,或者可以用在两个系统之间做交互,如果规则引擎的需求方是公司的运营人员或者业务人员,那么显然用 DSL 是不合适的。更好的做法是为他们提供一套 GUI,然后把用户点选的选项存储下来。这时候用 json 更为合适,也不需要你去写 parser 了。
你说你想自己造一个 json parser?
呵呵。
除了构造 AST,规则引擎剩下的工作就是在遍历 AST 的时候,能返回 true 或者 false。其实就是简单的 DFS,应届生都会写。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
《Unity3D网络游戏实战(第2版)》
罗培羽 / 机械工业出版社 / 2019-1-1 / 89.00元
详解Socket编程,搭建稳健的网络框架;解决网游中常见的卡顿、频繁掉线等问题;探求适宜的实时同步算法。完整的多人对战游戏案例,揭秘登录注册、游戏大厅、战斗系统等模块的实现细节。 想要制作当今热门的网络游戏,特别是开发手机网络游戏,或者想要到游戏公司求职,都需要深入了解网络游戏的开发技术。本书分为三大部分,揭示网络游戏开发的细节。 第一部分“扎基础”(1-5章) 介绍TCP网络游......一起来看看 《《Unity3D网络游戏实战(第2版)》》 这本书的介绍吧!
JS 压缩/解压工具
在线压缩/解压 JS 代码
MD5 加密
MD5 加密工具