计算器中,我们输入“20 + 10 - 5”,计算器会得出结果25并返回给我们。可你有没有想过计算器是怎样完成四则运算的?或者说,计算器是怎样识别你输入的这串字符串信息,并加以解析,然后执行之,得出结果?这里就引出了今天我想要介绍的一个设计模式----解释器模式。
1.解释器模式
解释器模式(Interpreter Pattern),给定一个语言,定义它的语法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 ----《大话设计模式》
解释器模式的基本思想是:对于每个符号,都定义一个表达式class表示它。通常情况下由这些class组成一个表达式的抽象语义树(Abstract Syntax Tree, AST),如下图所示:
Expr表示一个一个的表达式class,其中,黄色的表示终结符表达式(TermialExpression),粉色的表示非终结符表达式(NonTermialExpression)。从图中看,所谓终结符表达式就是就是叶节点,它不再有树枝;非终结符表达式有分支节点。我们可以以一个简单的算术表达式来说明解释器模式:
x + y - z
在以上表达式中,符号有 x、y、z、+、- 一共5个符号,其中x,y,z实际上都是变量,那么我们分为三种语义:变量Variable, 加法Plus, 减法Minus。 其实在计算机中一般是用后缀表达式来进行算数运算的,上面的表达式转换为后缀表达式是:
x y z - +
后缀、中缀、前缀表达式之间转换是有方法的,本文不做详细解释,按照后缀表达式,x + y - z运算的顺序实际上是这样的:
- y - z. --> v
- x + v
即 x + (y - z)
所以实际过程中,我们会生成这样由5个表达式实例组成的AST:
实际上这样的树形结构就是组合模式的应用。可参考组合模式。
一般地,解释器模式的UML类图如下:
图中,
TerminalExpression 即终结符表达式,没有子表达式, 在树形图中为黄色元素,像变量、常量等通常为终结符表达式。
NonTerminalExpression 即非终结符表达式,有子表达式, 在树形图中为粉色元素,像加法(plus),减法(minus)等通常包含多个子表达式(包含x, y)。
Context 环境的上下文信息,在本文中储存变量的信息。
2.代码实现
下面以实现一个支持加减法的程序为例来进一步直观展示解释器模式的应用。
抽象类或者接口,定义了interpret()
/** * 抽象表达式 */ interface Expression { int interpret(Map<String, Integer> context); }
实现加法表达式, right和left代表“+”符号的右边和左边的表达式, 因为包含其它表达式,所以为非终结符表达式。
/** * 定义了加法的法则 */ class Plus implements Expression { Expression right; Expression left; public Plus(Expression right, Expression left) { this.right = right; this.left = left; } @Override public int interpret(Map<String, Integer> context) { return right.interpret(context) + left.interpret(context); } }
实现减法,规则与加法略同
/** * 定义了减法的法则 */ class Minus implements Expression { Expression right; Expression left; public Minus(Expression r, Expression l) { right = r; left = l; } @Override public int interpret(Map<String, Integer> context) { return left.interpret(context) - right.interpret(context); } }
实现变量表达式,name是变量的名称,比如x,y,z,其值保存在context中,因为变量表达式不会再包含子表达式,所以其为终结符表达式。
class Variable implements Expression { String name; public Variable(String name) { this.name = name; } @Override public int interpret(Map<String, Integer> context) { return context.getOrDefault(name, 0); } }
转化并调用interpret(), 这里提供了parse()方法,将后缀表达式转化为AST,后缀表达式由页面或者客户端界面用户输入的中缀表达式转化而来,这里不做详解。
我将AST临时保存在stack中,将其出栈即可获取定义了整个AST的表达式实例,调用interpret即可。
public class InterpreterDemo { public static Expression parseToken(String token, ArrayDeque<Expression> stack) { Expression left, right; switch (token) { case "+": right = stack.pop(); left = stack.pop(); return new Plus(right, left); case "-": right = stack.pop(); left = stack.pop(); return new Minus(right, left); default: return new Variable(token); } } public static Expression parse(String expression) { ArrayDeque<Expression> stack = new ArrayDeque<>(); for (String token : expression.split(" ")) { stack.push(parseToken(token, stack)); } return stack.pop(); } public static void main(String[] args) { Expression expr = parse("x y z - +"); //context Map<String, Integer> context = new HashMap<>(); context.put("x", 20); context.put("y", 10); context.put("z", 5); //计算 int result = expr.interpret(context); System.out.println(result); } }
输出结果:25
3.总结
尽管在开发过程中使用解释器模式的场景不是很多,诸如正则表达式这类内置的功能使用解释器来解析语法得出我们想要的结果,因此,解释器模式其实就可以用下面这张图来理解:
输入一段表达式或者语句,按照规定的规则解释,将结果输出。一般情况下如下场景可以考虑使用解释器模式:
- 需要为一个简单的”语言“定义一套语法,
- 这门”语言“中的”句子“可以被解释。
这里,”语言“可以是编程语言,自然语言(或者人工构造语言),算术运算,某种规则等等。。只要有规律可循以定义语法或表达式。解释器模式的解决方案可以描述为:
- 定义Expression class的继承结构,并实现一个解释方法 interpret().
- 将各个Expression实例,组合成AST,以表示某一个句子或表达式。
- 使用AST中某个实例(通常为根部实例),调用interpret()方法翻译句子或表达式。
注意,解释器interpret()并不会将用户输入的原始句子转换为AST, 所以,调用interpret(), 首先需要先把原始语句转换为AST,通常可以提供一个转换器类parser,或者由客户端自行转换后再调用interpret().本例中,我在客户端封装了一个parse()方法来进行把语句"x y z - +"转换为AST。