解释器模式
解释器模式
案例
张三公司最近需要开发一款简单的加法/减法解释器,只要输入一个加法/减法表达式,它就能够计算出表达式结果,当输入字符串表达式为“1+2+3+4-5”时,将输出计算结果为3。很快张三就写了出来:
1.计算表达式类:
public class Calculator {
public int calculate(String expression) {
String[] expressionArray = expression.split("");
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < expressionArray.length; i++) {
if ("+".equals(expressionArray[i])) {
// 如果是 + 号,则再取下一个数累加后放入栈中
Integer num = stack.pop();
stack.push(num + Integer.valueOf(expressionArray[++i]));
} else if ("-".equals(expressionArray[i])) {
// 如果是 - 号,也是再取下一个数相减后放入栈中
Integer num = stack.pop();
stack.push(num - Integer.valueOf(expressionArray[++i]));
} else {
// 数字直接放入栈中
stack.push(Integer.valueOf(expressionArray[i]));
}
}
return stack.pop();
}
}
2.客户端使用:
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
String expression = "1+2+3+4-5";
System.out.println(expression + "=" +calculator.calculate(expression));
}
}
3.使用结果:
1+2+3+4-5=5
这里的例子比较简单,主要目的是为了介绍下面的解释器模式,它主要就是使用面向对象语言构成一个简单的语法解释器,如这里的"1+2+3+4-5",在 Java 中是不能直接解释运行的,必须自己定义一套文法规则来实现对这些语句的解释,相当于设计一个自定义语言。就像编译原理中语言和文法、词法分析、语法分析等过程。
模式介绍
一种行为型设计模式。定义了一个解释器,来解释给定语言和文法的句子。其实质是把语言中的每个符号定义成一个(对象)类,从而把每个程序转换成一个具体的对象树。
角色构成
- AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
- TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
- NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
- Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
UML 类图
解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。就是说在特定的应用场景中,可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。在这里的案例中"1+2+3+4-5",可以用如下文法规则来定义:
expression ::= value | operation
operation ::= expression '+' expression | expression '-' expression
value ::= an integer
首先这里的符号"::="表示“定义为”的意思。第一行表示的是一个表达式,表达式的组成方式为 value 和 operation,即操作和数字组成。第二行 operation 操作表示表达式相加或表达式相减。第三行就是指 value 是一个数字。下面就通过解释器模式来实现加法/减法解释器的功能。
代码改造
在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类。
1.所以第一步创建抽象表达式类:
// 抽象表达式类
public abstract class AbstractExpression {
// 提供统一的解释接口
public abstract int interpret();
}
2.各具体的终结或非终结符类:
数字解释器类
// 终结符类
public class ValueExpression extends AbstractExpression {
private int value;
public ValueExpression(int value) {
this.value = value;
}
@Override
public int interpret() {
return value;
}
}
加法非终结符类
// 非终结符类
public class AddExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public AddExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
减法非终结符类
// 非终结符类
public class SubtractionExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public SubtractionExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() - right.interpret();
}
}
3.计算器类:
// 计算器类
public class Calculator {
private AbstractExpression expression;
public void parse(String expression) {
String[] expressionArray = expression.split("");
Stack<AbstractExpression> stack = new Stack<>();
for (int i = 0; i < expressionArray.length; i++) {
if ("+".equals(expressionArray[i])) {
AbstractExpression left = stack.pop();
AbstractExpression right = new ValueExpression(Integer.parseInt(expressionArray[++i]));
stack.push(new AddExpression(left,right));
} else if ("-".equals(expressionArray[i])) {
AbstractExpression left = stack.pop();
AbstractExpression right = new ValueExpression(Integer.parseInt(expressionArray[++i]));
stack.push(new SubtractionExpression(left,right));
} else {
stack.push(new ValueExpression(Integer.parseInt(expressionArray[i])));
}
}
this.expression = stack.pop();
}
public int calculate() {
return expression.interpret();
}
}
4.客户端使用:
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
String expression = "1+2+3+4-5";
calculator.parse(expression);
System.out.println(expression + "=" + calculator.calculate());
}
}
5.使用结果
1+2+3+4-5=5
可以看到结果和上面是一样的。这里只是通过案例简单的运用了一下解释器模式,但不影响理解设计模式的魅力。
模式应用
虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。下面介绍的是其在Spring EL表达式中的典型应用。
1.首先引入这里需要的 Spring 包:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>design-pattern</artifactId>
<groupId>com.phoegel</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>interpreter</artifactId>
<properties>
<spring.version>5.1.15.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
2.简单的使用例子:
public class Main {
public static void main(String[] args) {
SpelExpressionParser parser = new SpelExpressionParser();
String expressionStr = "1+2+3+4-5";
Expression expression = parser.parseExpression(expressionStr);
System.out.println(expressionStr + "=" + expression.getValue());
}
}
3.使用结果:
1+2+3+4-5=5
可以看到这里主要使用了类SpelExpressionParser.parseExpression()
方法返回了Expression
实例,看到这个类名就感觉和解释器模式很有关系,因此由此深入源码会发现Expression
是一个接口,它的具体实现类有CompositeStringExpression
、LiteralExpression
和SpelExpression
等。而这里使用的SpelExpressionParser
类来获取Expression
,因此很明显它将返回SpelExpression
,通过追踪源码也可以证明这一点。
下面是SpelExpressionParser
类中获取Expression
的关键代码,它其实最终通过InternalSpelExpressionParser
类中doParseExpression()
方法返回的:
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
try {
this.expressionString = expressionString;
// 根据传入的字符串生成 Tokenizer 类实例
Tokenizer tokenizer = new Tokenizer(expressionString);
// 分析词法生成 List<Token> 集合,在这里类似于将字符串"1+2+3+4-5"分成了一个一个的次,类似于:1、+、2、+、3......
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
this.tokenStreamPointer = 0;
this.constructedNodes.clear();
// 生成抽象语法树 ast
SpelNodeImpl ast = eatExpression();
Assert.state(ast != null, "No node");
Token t = peekToken();
if (t != null) {
throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
}
Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
// 最后将上面的数据封装入 SpelExpression 类实例并返回
return new SpelExpression(expressionString, ast, this.configuration);
}
catch (InternalParseException ex) {
throw ex.getCause();
}
}
上面代码中最重要的是调用了tokenizer.process()
生成词法集合,以及eatExpression()
方法生成了 ast 抽象语法树。可以看到 ast 的类型为SpelNodeImpl
,它的子类主要有 Literal,Operator,Indexer等,其中 Literal 是各种类型的值的父类,Operator 则是各种操作的父类。通过运行时的查看,能够看到这里的属性如下图示:
根据上面的图可以详细的看出整个 ast 抽象语法数的结构及其每个节点的组成。最后就是通过expression.getValue()
方法获取结果了,代码如下:
public Object getValue() throws EvaluationException {
CompiledExpression compiledAst = this.compiledAst;
// 这里的 compiledAst == null ,所以不会进入判断
if (compiledAst != null) {
try {
EvaluationContext context = getEvaluationContext();
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
}
}
}
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration);
// 直接调用 this.ast.getValue() 获取值
Object result = this.ast.getValue(expressionState);
checkCompile(expressionState);
return result;
}
可以看到获取值的时候是调用 ast 中的getValue()
方法的,而这里的 ast 语法树的节点类型从上面的 ast 抽象语法树结构图可以看出来是OpMinus
类实例,因此会调用OpMinus
类中的getValue()
方法,这个方法的核心就是计算减法两边表达式的值相减返回结果,由于getValue()
较长,这里只贴出关键代码:
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
SpelNodeImpl leftOp = getLeftOperand();
// 判断类型代码省略...
Object left = leftOp.getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
if (left instanceof Number && right instanceof Number) {
Number leftNumber = (Number) left;
Number rightNumber = (Number) right;
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
}
// 其他判断类型代码省略...
else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) {
// 这里是数字做运算,所以会进入这里并返回相减结果
this.exitTypeDescriptor = "I";
return new TypedValue(leftNumber.intValue() - rightNumber.intValue());
}
else {
// Unknown Number subtypes -> best guess is double subtraction
return new TypedValue(leftNumber.doubleValue() - rightNumber.doubleValue());
}
}
// ...
return state.operate(Operation.SUBTRACT, left, right);
}
整个 SpringEl 表达式中相关联的类非常多,这里只是根据例子进行了源码追踪,来更好的理解解释器模式。
总结
主要优点
-
易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
-
每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
-
实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。
-
增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。
主要缺点
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
适用场景
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 一个语言的文法较为简单。
- 执行效率不是关键问题。【注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。】
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 设计模式 | 解释器模式及典型应用
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/interpreter
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/14140676.html