為什么要使用DSL
DSL是領域專用語言,常見的DSL有SQL,CSS,Shell等等,這些DSL語言有別於其他通用語言如:C++,Java,C#,DSL常在特殊的場景或領域中使用。如下圖:
領域專用語言通常是被領域專家使用,領域專家一般不熟悉通用編程語言,但是他們一般對業務非常了解,程序員一般對通用語言比較熟悉,但是在做行業軟件的時候對業務部了解。這就需要協作的過程,一種方式是領域專家通過文檔或者教授的方式把業務邏輯傳遞給程序員讓程序員翻譯成業務邏輯,而另一種方法,程序員為領域專家定制DSL,並編寫解釋DSL的環境嵌入在業務系統中。這樣在某塊功能的實現上,程序員可以不用去關系具體實現和業務,而領域專家也不用過多的理解程序背后的事情。
這種需求常常出現在OA系統或ERP系統的工作流中。比如說部門申請單的審批,如果是OA產品,那么這個審批流程將面對不同企業各式各樣的審批的條件,一個企業中不同的部門審批的條件也不一樣。如果全靠程序在后台死寫,那么不可能窮盡用戶的想法,那么遇見這類對性能要求不高,又需要很強的靈活性的需求,通常會用到DSL,讓用戶輸入類似的業務邏輯:[部門]=”人事部” AND [金額] <= 1000 通過。
在舉個例子,在車聯網系統中,我們需要判斷一輛車是否在經濟區中運行,這個業務邏輯判斷的因素比較多,常常不是程序員或者產品經理可以寫出來的,需要交給車輛專家來編寫。也許會寫成這樣: ( [天氣]!=”下雨” AND 50< [車速] <= 80 ) OR ( [道路] ==”高速” AND 60< [車速] <= 110 )。這同樣需要我們把他翻譯成我們系統實現的代碼。
如果上述的功能比較簡單,DSL也不會很復雜,那么我們只需要簡單的解釋器模式就可以解決。但是如果遇見的業務比較復雜且變化比較多,那么使用工具來解析DSL將是必然的選擇。
常見的語法分析器代碼生成工具有yacc,lexer,antlr,T4等等。yacc采用的是LALR(1),而antlr采用LL(k)的解析方法。對詞法分析,語言分析,AST或者編譯原理有了解的話,有助於這些工具的使用。
Antlr的安裝
Antlr可以生成C#,Java'和其他一些語言的解析工具代碼。我這里使用C#做例子,可以在NuGet(Java就是在Maven)中下載最新版本Antlr的DLL,Antlr,Antlr4.Runtime.並且下載Antlr在VisioStudio的項目模板(在VS中Tools->Extensions and Updates)。如果你使用的VS項目模板那么你可以在項目添加g4后綴的文件,antlr詞法和語言生成工具的文法文件都是使用g4為后綴。如下圖,對於小型項目我們一般使用Combined Grammar,詞法和語法都放在一起。
可以參考如下地址:https://github.com/tunnelvisionlabs/antlr4cs
在新建的g4中編輯語法,保存並編譯,就會在項目路徑下的obj\Debug目錄下生成語法解析和詞法解析的基類代碼。
Antlr的語法簡介
最新的g4版本的語言可以參看官方文檔:,如果需要更加系統的學習的話,需要下載最新的antlr4的官方書籍antlr book 4,免費的電子書可以百度搜索”The Definitive ANTLR 4 Reference”。
Antlr實例
以在車聯網系統中,判定車輛是否超速為例子。每個用戶或者說是企業都需要管理自己所有的車輛,在業務系統中,也會對車輛是否超速給出一個定義。這個定義也許不會想[車速]>80這么簡單,有時候還會出現如下的定義:”(([車速]*10+3)>(200)) && ([企業ID] == \"123\") && ([時間]>1200 && [時間]<1700)”。從這個例子中可以看出,判定超速的規則支持四則混合運算,還有一些特定的變量如車速,企業ID,時間。這中類型的定義是我們系統期望的讓每個用戶定義的方式。因為這種方式足夠靈活。用戶可以隨意配置。為了實現這種方式,解釋器模式是一個可行的方案,但是如果我們使用DSL,則更加靈活和可擴展。我們定義的這種DSL,不單單執行上述的四則混合運算,還必須支持變量。這些變量都是我們在真是的系統運行中需要去獲取(數據庫或者緩存)的,也就是說,我們的解析程序首先要獲取這些變量的值,然后再進行運算,最后得出一個是否超速的結果。當然隨着我們DSL的解析越來越完善,算法越來越先進,支持的變量也許會更多,也許還會有道路等級,天氣因素等算法因子的出現。
要實現這個需求首先我們要定義文法,也就是g4文件的內容。
注意的是,在一些文法后面用”#”號定義了一個名稱,就會在用於訪問生成的抽象語法樹AST的訪問器中生成該方法,用於訪問當這個規約被滿足時候的那個樹節點。
grammar ISL;
@header
{
using System;
}
@members
{
}
/*
* Parser Rules
*/
/*
* 表達式
*/
expression
: NUMBER #Number
| STRING #String
| VARIABLE #Variable
| SUB expression #SubExpr
| expression op=(MUL|DIV) expression #MulDiv
| expression op=(ADD|SUB) expression #AddSub
| LEFT_PAREN expression RIGHT_PAREN #Paren
;
equality_expression
: TRUE #LogicalTrue
| FALSE #LogicalFalse
| expression op=(GREATE_THAN | GREATE_EQUAL_THAN | LESS_THAN | LESS_EQUAL_THAN | EQUAL | NOT_EQUAL) expression #LogicalOp
| equality_expression op=(LOGICAL_NOT | LOGICAL_AND | LOGICAL_OR | EQUAL | NOT_EQUAL) equality_expression #LogicalAndOrNot
| LEFT_PAREN equality_expression RIGHT_PAREN #Paren2
;
/*
* 返回語句
*/
return_statement
: RETURN equality_expression SEMICOLON #Return
;
elseif_list
: elseif+
//| elseif_list elseif
;
elseif
: ELSEIF LEFT_PAREN expression RIGHT_PAREN block
;
if_statement
: IF LEFT_PAREN expression RIGHT_PAREN block
| IF LEFT_PAREN expression RIGHT_PAREN block ELSE block
| IF LEFT_PAREN expression RIGHT_PAREN block elseif_list
| IF LEFT_PAREN expression RIGHT_PAREN block elseif_list ELSE block
;
statement
: expression SEMICOLON
| if_statement
;
block
: LEFT_CURLY statement_list RIGHT_CURLY
| LEFT_CURLY RIGHT_CURLY
;
statement_list
: statement+
;
/*
* Lexer Rules
*/
VARIABLE : '[車速]' | '[天氣]' | '[時間]' | '[企業ID]' | '[用戶ID]'; // 數字變量
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ; // 數字
STRING : '"' ('\\"'|.)*? '"' ; // 字符串
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
ADD : '+' ;
SUB : '-' ;
MUL : '*' ;
DIV : '/' ;
MOD : '%' ;
GREATE_THAN : '>' ;
GREATE_EQUAL_THAN : '>=' ;
LESS_THAN : '<' ;
LESS_EQUAL_THAN : '<=' ;
EQUAL : '==' ;
TRUE : 'true' ;
FALSE : 'false' ;
NOT_EQUAL : '!=' ;
LOGICAL_AND : '&&' ;
LOGICAL_OR : '||' ;
LOGICAL_NOT : '!' ;
LEFT_PAREN : '(' ;
RIGHT_PAREN : ')' ;
LEFT_CURLY : '{' ;
RIGHT_CURLY : '}' ;
CR : '\n' ;
IF : 'if' ;
ELSE : 'else' ;
ELSEIF : 'else if' ;
SEMICOLON : ';' ;
DOUBLE_QUOTATION : '"' ;
RETURN : 'return' ;
LINE_COMMENT : '//' .*? '\n' -> skip ;
COMMENT : '/*' .*? '*/' -> skip ;
生成好代碼之后,我們使用Visitor訪問器(參看The Definitive ANTLR 4 Reference這本書)來實現語法樹的訪問。
public class ISLVisitor2 : ISLBaseVisitor<Result>
{
public override Result VisitNumber(ISLParser.NumberContext context)
{
Result r = new Result();
r.Value = double.Parse(context.NUMBER().GetText());
r.Text = context.NUMBER().GetText();
return r;
}
public override Result VisitParen(ISLParser.ParenContext context)
{
Result o = Visit(context.expression());
o.Text = "(" + o.Text + ")";
return o;
}
public override Result VisitParen2(ISLParser.Paren2Context context)
{
Result o = Visit(context.equality_expression());
o.Text = "(" + o.Text + ")";
return o;
}
public override Result VisitMulDiv(ISLParser.MulDivContext context)
{
Result r = new Result();
double left = Convert.ToDouble(Visit(context.expression(0)).Value);
double right = Convert.ToDouble(Visit(context.expression(1)).Value);
if (context.op.Type == ISLParser.MUL)
{
r.Value = left * right;
r.Text = Visit(context.expression(0)).Text + " 乘以 " + Visit(context.expression(1)).Text;
}
if (context.op.Type == ISLParser.DIV)
{
r.Value = left / right;
r.Text = Visit(context.expression(0)).Text + " 除以 " + Visit(context.expression(1)).Text;
}
return r;
}
public override Result VisitAddSub(ISLParser.AddSubContext context)
{
Result r = new Result();
double left = (double)Visit(context.expression(0)).Value;
double right = (double)Visit(context.expression(1)).Value;
if (context.op.Type == ISLParser.ADD)
{
r.Value = left + right;
r.Text = Visit(context.expression(0)).Text + " 加上 " + Visit(context.expression(1)).Text;
}
else
{
r.Value = left - right;
r.Text = Visit(context.expression(0)).Text + " 減去 " + Visit(context.expression(1)).Text;
}
return r;
}
public override Result VisitVariable(ISLParser.VariableContext context)
{
Result r = new Result();
if (context.GetText() == "[車速]")
{
r.Text = "車速";
r.Value = TestData.VehicleSpeed;
}
else if (context.GetText() == "[天氣]")
{
r.Text = "天氣";
r.Value = TestData.Weather;
}
else if (context.GetText() == "[時間]")
{
r.Text = "時間";
r.Value = TestData.Now;
}
else if (context.GetText() == "[企業ID]")
{
r.Text = "企業ID";
r.Value = TestData.EntId;
}
else if (context.GetText() == "[用戶ID]")
{
r.Text = "用戶ID";
r.Value = TestData.AccountId;
}
return r;
}
public override Result VisitLogicalFalse(ISLParser.LogicalFalseContext context)
{
Result r = new Result();
r.Value = false;
return r;
}
public override Result VisitLogicalTrue(ISLParser.LogicalTrueContext context)
{
Result r = new Result();
r.Value = true;
return r;
}
public override Result VisitLogicalAndOrNot(ISLParser.LogicalAndOrNotContext context)
{
Result r = new Result();
if (context.op.Type == ISLParser.LOGICAL_AND)
{
bool o1 = Convert.ToBoolean(Visit(context.equality_expression(0)).Value);
bool o2 = Convert.ToBoolean(Visit(context.equality_expression(1)).Value);
r.Value = o1 && o2;
r.Text = Visit(context.equality_expression(0)).Text + " 並且 " + Visit(context.equality_expression(1)).Text;
}
return r;
}
public override Result VisitString(ISLParser.StringContext context)
{
Result r = new Result();
r.Value = context.GetText().Replace("\"", "");
r.Text = context.GetText().Replace("\"", "");
return r;
}
public override Result VisitLogicalOp(ISLParser.LogicalOpContext context)
{
Result r = new Result();
object result = null;
if (context.op.Type == ISLParser.GREATE_THAN)
{
double left = Convert.ToDouble(Visit(context.expression(0)).Value);
double right = Convert.ToDouble(Visit(context.expression(1)).Value);
if (left > right)
{
result = 1;
}
else
{
result = 0;
}
r.Text = Visit(context.expression(0)).Text + " 大於 " + Visit(context.expression(1)).Text;
}
if (context.op.Type == ISLParser.LESS_THAN)
{
double left = Convert.ToDouble(Visit(context.expression(0)).Value);
double right = Convert.ToDouble(Visit(context.expression(1)).Value);
if (left < right)
{
result = 1;
}
else
{
result = 0;
}
r.Text = Visit(context.expression(0)).Text + " 小於 " + Visit(context.expression(1)).Text;
}
if (context.op.Type == ISLParser.EQUAL)
{
object left = Visit(context.expression(0)).Value;
object right = Visit(context.expression(1)).Value;
if (left is string)
{
result = left.ToString() == right.ToString();
}
else
{
result = Visit(context.expression(0)).Value == Visit(context.expression(1)).Value;
}
r.Text = Visit(context.expression(0)).Text + " 等於 " + Visit(context.expression(1)).Text;
}
r.Value = result;
return r;
}
public override Result VisitReturn(ISLParser.ReturnContext context)
{
Result o = Visit(context.equality_expression());
return o;
}
}
public class Result
{
public string Text { get; set; }
public object Value { get; set; }
}
class Program { static void Main(string[] args) { TestISL(); Console.ReadLine(); } private static void TestISL() { string text = string.Empty; ParseISL(""); } private static void ParseISL(string input) { input = "return (([車速]*10+3)>(200)) && ([企業ID] == \"123\") && ([時間]>1200 && [時間]<1700);"; AntlrInputStream inputStream = new AntlrInputStream(input); ISLLexer lexer = new ISLLexer(inputStream); CommonTokenStream tokens = new CommonTokenStream(lexer); ISLParser parser = new ISLParser(tokens); IParseTree tree = parser.return_statement(); //ISLVisitor visitor = new ISLVisitor(); //object ret = visitor.Visit(tree); ISLVisitor2 visitor = new ISLVisitor2(); Result ret = visitor.Visit(tree); //Console.WriteLine(ret); Console.WriteLine(ret.Value); Console.WriteLine(ret.Text); Console.ReadLine(); } }
最后,點擊這里下載示例。


![]H~)K6@95W)YNYQ~U(PR%BJ ]H~)K6@95W)YNYQ~U(PR%BJ](/image/aHR0cHM6Ly9pbWFnZXMwLmNuYmxvZ3MuY29tL2Jsb2cvMTU3MDAvMjAxNTAxLzE0MjIxNzA1OTAxOTg4Ny5qcGc=.png)
