简介
解释器模式是一种行为型设计模式,它提供了一种构建抽象语法树的机制,并定义了如何解释这棵树。解释器模式属于编译原理中的语法制导翻译的范畴。
如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。
//用于两个整数相加
public static int add(int a,int b){
return a + b;
}
//用于两个整数相加
public static int add(int a,int b,int c){
return a + b + c;
}
//用于n个整数相加
public static int add(Integer ... arr) {
int sum = 0;
for (Integer i : arr) {
sum += i;
}
return sum;
}
上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。
显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。
定义:
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和±符号组成的合法序列,“1+3-2” 就是这种语言的句子。
解释器就是要解析出来语句的含义。但是如何描述规则呢?
文法(语法)规则:
文法是用于描述语言的语法结构的形式规则。
expression ::= value | plus | minus
plus ::= expression ‘+’ expression
minus ::= expression ‘-’ expression
value ::= integer
注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。
上面规则描述为 :
表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。
抽象语法树:
在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
用树形来表示符合文法规则的句子。
结构
解释器模式包含以下主要角色。
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
案例实现
【例】设计实现加减法的软件
代码如下:
//抽象角色AbstractExpression
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}
//终结符表达式角色
public class Value extends AbstractExpression {
private int value;
public Value(int value) {
this.value = value;
}
@Override
public int interpret(Context context) {
return value;
}
@Override
public String toString() {
return new Integer(value).toString();
}
}
//非终结符表达式角色 加法表达式
public class Plus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}
///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}
//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
private String name;
public Variable(String name) {
this.name = name;
}
@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}
@Override
public String toString() {
return name;
}
}
//环境类
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();
public void assign(Variable var, Integer value) {
map.put(var, value);
}
public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}
//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
//Value v = new Value(1);
context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
context.assign(e, 5);
AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);
System.out.println(expression + "= " + expression.interpret(context));
}
}
优缺点
1,优点:
-
易于改变和扩展文法。
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
-
实现文法较为容易。
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
-
增加新的解释表达式较为方便。
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。
2,缺点:
-
对于复杂文法难以维护。
在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
-
执行效率较低。
由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
使用场景
-
当语言的文法较为简单,且执行效率不是关键问题时。
-
当问题重复出现,且可以用一种简单的语言来进行表达时。
-
当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。
例:
- 数学表达式计算
- 类似于计算器程序,可以使用解释器模式来解析和执行数学表达式,如加减乘除等基本运算。通过定义不同的表达式类(如加法表达式、减法表达式等),以及一个共同的解释接口,可以构建一个灵活且可扩展的数学表达式求值器。
- 文本处理
- 对于需要解析和处理特定格式的文本文件(如配置文件、日志文件等),解释器模式可以定义这些文件的语法规则,并通过解释器对象来解析和处理文件内容。
- SQL解析
- SQL解析器是一个典型的解释器模式应用。它将SQL语句解析为执行计划和查询结果,通过定义SQL语句的语法规则和相应的解释器对象,可以实现对SQL语句的灵活解析和执行。
- 正则表达式
- 正则表达式解析器也是解释器模式的一个应用实例。正则表达式定义了字符串的搜索模式,解释器模式可以实现对这些模式的解析和匹配。
- 数学表达式计算
源码中的应用
JDK
正则表达式(Regular Expressions)
Java中的正则表达式库(java.util.regex
包)是解释器模式的一个典型应用。在这个库中,Pattern
类可以被视为一个解释器,它负责解析正则表达式并生成一个与之对应的Matcher
对象,后者用于对目标字符串进行匹配操作。
- 抽象表达式(Abstract Expression):在正则表达式的上下文中,这可以看作是正则表达式的整体结构和规则,这些规则在
Pattern
类中通过内部的数据结构和算法来表示。 - 终结符表达式(Terminal Expression):正则表达式中的字符和字符类(如
a
、\d
、.
等)可以被视为终结符表达式,它们代表了最基础的匹配单元。 - 非终结符表达式(Nonterminal Expression):正则表达式中的量词(如
*
、+
、?
)和组合操作(如括号()
)等则属于非终结符表达式,它们用于控制匹配的重复性和组合性。 - 上下文(Context):在正则表达式的匹配过程中,上下文信息通常包括被匹配的字符串、匹配的位置以及正则表达式的标志(如是否区分大小写)等。
Spring
SpEL表达式
SpEL 是一种功能强大的表达式语言,用于在运行时查询和操作对象图。它提供了一种在字符串表示式中表示复杂逻辑的简便方式,并且可以在运行时对这些字符串求值。这种机制正是解释器模式的一个典型应用。
- 在Spring中,
ExpressionParser
接口扮演了抽象表达式的角色,它定义了一个parseExpression
方法,用于将字符串形式的表达式解析成可以执行的表达式对象(即解释器)。 - 具体的表达式对象(如通过
parseExpression
返回的)则实现了表达式的具体执行逻辑,这可以看作是解释器模式的解释器角色。
.Net Core
Razor 视图引擎
Razor 视图引擎是 ASP.NET Core 中用于生成 HTML 输出的一个关键组件。虽然 Razor 本身并不完全遵循解释器模式的经典结构,但它确实包含了对 Razor 模板(一种类似 HTML 但包含 Razor 语法的标记语言)的解析和执行逻辑。
- Razor 页面或视图被编译成 C# 代码,并在运行时执行。这个过程中,Razor 模板中的表达式、代码块和指令等被识别并转换成相应的 C# 代码。
- 这种转换和执行机制可以看作是解释器模式的一种变体,其中 Razor 编译器充当了解释器的角色,而 Razor 模板则是被解释的“语言”。
表达式树(Expression Trees)
虽然表达式树本身并不是解释器模式的一部分,但它们是 .NET 中实现动态代码生成、查询优化和表达式解析等高级功能的基础。开发者可以使用表达式树来表示和操作代码结构,这些表达式树可以在运行时被解释或编译成可执行代码。
- 在某些情况下,开发者可能会自己编写解释器来遍历和执行表达式树中的表达式。虽然这超出了 .NET Core 源码的直接范围,但它展示了表达式树如何与解释器模式结合使用的可能性。