【行为型模式十】解释器模式
一、定义一种计算公式模型
现在有一个需求,要实现输入一个模型公式(例如输入a+b+c-d或a+b+e-d或a-f型的公式),然后再输入模型中的参数值(例如a为1,b为2,c为3,d为4,则依次输入1,2,3,4),运算出结果。要求在程序运行期间可解析输入的不同公式,并给出运算结果。
需求不复杂,若仅仅对数字采用四则运算,每个程序员都可以写出来。但是增加了增加模型公式就复杂了。先解释一下为什么需要公式, 而不采用直接计算的方法,例如有如下3个公式:
业务种类1的公式:a+b+c-d;
业务种类2的公式:a+b+e-d;
业务种类3的公式:a-f。
其中,a、b、c、d、e、f参数的值都可以取得,如果使用直接计算数值的方法需要为每个品种写一个算法,目前仅仅是3个业务种类,那上百个品种呢?歇菜了吧!建立公式,然后通过公式运算才是王道。
我们以实现加减算法(由于篇幅所限,乘除法的运算读者可以自行扩展)的公式为例,讲解如何解析一个固定语法逻辑。由于使用语法解析的场景比较少,而且一些商业公司(比如SAS、SPSS等统计分析软件)都支持类似的规则运算,亲自编写语法解析的工作已经非常少,以下例程采用逐步分析方法,带领大家了解这一实现过程。
我们来想,公式中有什么?仅有两类元素:运算元素和运算符号,运算元素就是指a、b、c等符号,需要具体赋值的对象,也叫做终结符号,为什么叫终结符号呢?因为这些元素除了需要赋值外,不需要做任何处理,所有运算元素都对应一个具体的业务参数,这是语法中最小的单元逻辑,不可再拆分;运算符号就是加减符号,需要我们编写算法进行处理,每个运算符号都要对应处理单元,否则公式无法运行,运算符号也叫做非终结符号。两类元素的共同点是都要被解析,不同点是所有的运算元素具有相同的功能,可以用一个类表示,而运算符号则是需要分别进行解释,加法需要加法解析器,减法也需要减法解析器。
这种需要按照规定语法进行解析的需求,就可以使用解释器模式来实现。
二、解释器模式
解释器模式(Interpreter),定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的 “语言” 是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。
解释器模式实现了一种专门的语言,在现在项目中使用较少(谁没事干会去写一个PHP或者Ruby的解析器)。
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释、运算表达式计算等领域还是得到了广泛使用。
2.1 解释器模式的角色
使用解释器模式实现公式解析的类图:
AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
三、使用解释器模式实现公式解析
3.1 抽象表达式
public abstract class Expression {
//用map是因为要把先传入的公式中的字母与后传入的数字参数对应起来
//interpret负责对对传递进来的参数和值进行解析和匹配,其中输入参数为HashMap类型,key值为模型中的参数,如a、b、c等,value为运算时取得的具体数字。
public abstract int interpret(Map<String,Integer> expressionMap);
}
3.2 终结符表达式
数字表达式
public class NumExpression extends Expression {
private String numStr;
public NumExpression(String numStr) {
this.numStr = numStr;
}
@Override
public int interpret(Map<String, Integer> expressionMap) {
return expressionMap.get(numStr);
}
}
3.3 非终结符表达式
- 加法操作表达式
public class SumExpression extends OperationExpression {
public SumExpression(Expression leftExpression, Expression rightExpression) {
super(leftExpression, rightExpression);
}
//不算递归,因为leftExpression和rightExpression的interpret方法未递归调用,否则必须给定递归终止条件
@Override
public int interpret(Map<String, Integer> expressionMap) {
return leftExpression.interpret(expressionMap) + rightExpression.interpret(expressionMap);
}
}
- 减法操作表达式
public class SubExpression extends OperationExpression {
public SubExpression(Expression leftExpression, Expression rightExpression) {
super(leftExpression, rightExpression);
}
@Override
public int interpret(Map<String, Integer> expressionMap) {
return leftExpression.interpret(expressionMap) - rightExpression.interpret(expressionMap);
}
}
3.4 计算器类
使用了一个栈来保存表达式。
public class SimpleCalculator {
private Stack<Expression> numberStack = new Stack<>();
private Map<String,Integer> expressionMap;
private Expression leftExpression;
private Expression rightExpression;
private NumExpression numExpression;
public SimpleCalculator(Map<String,Integer> expressionMap) {
this.expressionMap = expressionMap;
}
public int compute(char[] formula) throws Exception {
int index = 0;
while (index < formula.length-1) {
if (isLetter(formula[index])) {
numExpression = new NumExpression(String.valueOf(formula[index]));
numberStack.push(numExpression);
} else {
leftExpression = numberStack.pop();
rightExpression = new NumExpression(String.valueOf(formula[index+1]));
OperationExpression operationExpression = getOperationExpression(formula[index]);
numberStack.push(operationExpression);
index++;//下个num已经被使用过了,需跳过
}
index++;
}
return numberStack.pop().interpret(expressionMap);
}
private OperationExpression getOperationExpression(char operationChr) throws Exception {
switch (operationChr) {
case '+' :
return new SumExpression(leftExpression, rightExpression);
case '-' :
return new SubExpression(leftExpression, rightExpression);
default:
throw new Exception("暂不支持的运算符 " + operationChr);
}
}
}
3.5 工具类
public class ExpressionUtil {
public static Map<String,Integer> genExpressionMap(String expressionStr,int[] nums) throws Exception {
if (nums.length != (expressionStr.length()+1)/2) {
throw new Exception("运算变量个数与参数个数不符!");
}
Map<String,Integer> expressionMap = new HashMap<>();
if (expressionStr == null || expressionStr.trim().equals("")) {
throw new Exception("公式为空!");
}
char[] expressionChars = expressionStr.toCharArray();
if (!isLetter(expressionChars[0])) {
throw new Exception("公式需以字母开头!");
}
int chrIndex = 0;
int numIndex = 0;
while (chrIndex < expressionChars.length-1) {
char chr = expressionChars[chrIndex];
if (isLetter(chr)) {
if (expressionMap.containsKey(String.valueOf(chr))) {
throw new Exception("公式中包含了重复的变量"+chr);
}else if (isLetter(expressionChars[chrIndex+1])) {
throw new Exception(chr+","+expressionChars[chrIndex+1]+"这两个变量之间需使用运算符隔开!");
} else {
expressionMap.put(String.valueOf(chr),nums[numIndex++]);
}
}else if (!isLetter(expressionChars[chrIndex+1])){
throw new Exception(chr+","+expressionChars[chrIndex+1]+"这两个操作符之间需使用变量隔开");
}
chrIndex++;
}
char lastChar = expressionChars[chrIndex];
if (isLetter(lastChar)) {
if (expressionMap.containsKey(String.valueOf(lastChar))) {
throw new Exception("公式中包含了重复的变量" + lastChar);
}else {
expressionMap.put(String.valueOf(lastChar),nums[numIndex]);
}
}else {
throw new Exception("公式需以字母结尾!");
}
return expressionMap;
}
}
3.6 客户端类
public class Client {
/**
* 使用解释器模式实现新语言:运算公式的解析
*/
public static int computeByInterpreter(String expressionStr,int[] nums) throws Exception {
Map<String,Integer> expressionMap = ExpressionUtil.genExpressionMap(expressionStr,nums);
SimpleCalculator calculator = new SimpleCalculator(expressionMap);
return calculator.compute(expressionStr.toCharArray());
}
public static void main(String[] args) {
String expressionStr = "a+b-c";
int[] nums = {1,2,4};
try {
System.out.println(computeByInterpreter(expressionStr,nums));
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 运行结果
-1
main方法中写死的表达式和参数实际上应通过外部输入,就可以实现运行时解析各种不同的公式了。
四、解释器模式总结
4.1 优点
- 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。 - 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。
4.2 缺点
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
#五、解释器模式适用场景 - 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 一个语言的文法较为简单。
- 对执行效率要求不高。