解释器模式(
interpreter
)给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器用来解释语言中的句子。
这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。
比如我们常常会在字符串中搜索匹配的字符或判断一个字符串是否符合我们的规则,此时一般我们会用什么技术?
如判断email
、匹配电话号码等。我们会用到正则表达式,而所谓解释器模式,正则表达式就是它的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。
一、解释器模式类图
二、解释器模式角色
- 抽象表达式 (AbstractExpression)
定义解释器接口,解释器的解释操作,主要包含interpret()方法。
- 终结符表达式 (TermianExpression)
实现与文法中的终结符相关联的解释操作。实现抽象表达式所要求的接口。文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式 (NonterminalExpression)
用来实现文法中与终结符相关的操作,文法中的每条规则都对应一个非终结符表达式。
- 环境角色(Context)
通常包含各个解释器需要的数据或公共的功能。
- 客户端 (Client)
客户端代码,构建表示该文法定义的语言中一个特定的句子的抽象语法树,调用解释操作。
三、小例子
这是一个加减乘除的小例子。
- 抽象表达式 (AbstractExpression)
//抽象的解释器,他提供了元素与元素之间的计算方式的类以及真正的计算方式。
public interface Node {
public int interpret();
}
- 终结符表达式 (TermianExpression)
//终结解释器。就是为了返回最终的值。这个node都能够代表数学表达式中数字部分的展现。
public class ValueNode implements Node{
private int value;
public ValueNode( int value) {
this.value = value;
}
@Override
public int interpret() {
return this.value;
}
}
- 非终结符表达式 (NonterminalExpression)
//非终结解释器,他需要关联我们的node,
public abstract class SymbolNode implements Node{
//分为左node和右node。比如 2*3.2在左边,3在右边。2和3同
//时输入乘法的node。
Node left;
Node right;
public SymbolNode(Node left,Node right) {
this.left = left;
this.right = right;
}
}
public class MulNode extends SymbolNode{
public MulNode(Node left, Node right) {
super(left, right);
}
@Override
public int interpret() {
return left.interpret() * right.interpret();
}
}
public class DivNode extends SymbolNode{
public DivNode(Node left, Node right) {
super(left, right);
}
@Override
public int interpret() {
return left.interpret() / right.interpret();
}
}
public class ModNode extends SymbolNode{
public ModNode(Node left, Node right) {
super(left, right);
}
@Override
public int interpret() {
return left.interpret() % right.interpret();
}
}
- 核心处理类
/**
* 结合了Context类,并且为客户端提供了统一调用接口
* 这个类是我们的解释器核心
*/
public class Calculator {
private Node node;
private String statement;
/**
* build方法,解释了我们的计算公式,将解释的结果存入stack,也就是我们的环境类,最终从stack中取出,转化成我们的node类,然后执行我们的interupte方进行计算。
*/
public void build(String statement){
//结合了我们的非终结解释器
Node left =null;
Node right =null;
Stack stack = new Stack(); //提供环境,存储一些关系
//我们最重要将我们的node存储到 stack中。存储之前,我们已经确定了表达式的顺序,解释完成的结果。
String[] statementArr = statement.split(" ");
for (int i = 0; i < statementArr.length; i++) {
if (statementArr[i].equalsIgnoreCase("*")) {
left = (Node)stack.pop();//pop这个方法,显示栈顶元素并且移除栈顶元素
int val = Integer.parseInt(statementArr[++i]);
right = new ValueNode(val);
stack.push(new MulNode(left, right));//mutinode代表乘号,left与right代表2 和 3
}
else if (statementArr[i].equalsIgnoreCase("/")) {
left = (Node)stack.pop();
int val = Integer.parseInt(statementArr[++i]);
right = new ValueNode(val);
stack.push(new DivNode(left, right));//mutinode代表/,left与right代表2 和 3
}
else if (statementArr[i].equalsIgnoreCase("%")) {
left = (Node)stack.pop();
int val = Integer.parseInt(statementArr[++i]);
right = new ValueNode(val);
stack.push(new ModNode(left, right));//mutinode代表%,left与right代表2 和 3
}
else{
stack.push(new ValueNode(Integer.parseInt(statementArr[i]))); //传入的数字
}
}
this.node = (Node) stack.pop(); //这个node包含了所有的数字以及所有的符号
}
public int compute(){
return node.interpret();
}
}
- 测试类
public class Test {
public static void main(String[] args) {
String statement = "3 * 2 * 4 / 3 % 5";
Calculator calculator = new Calculator();
calculator.build(statement);
System.out.println(statement+" = "+calculator.compute());
}
}
- 测试结果
3 * 2 * 4 / 3 % 5 = 3
-
代码解析
这张图是通过debug
的方式进行查看如何计算的,首先会进行划分(3*2*4/3)%5
,在进行划分((3*2*4)/3)%5
是以这种树形结构进行计算的,先计算里边内容的值,在用计算的和再去外边与外边的数值进行计算,如果大家不是很理解,可以查看GitHub中代码运行,会更加容易理解。
四、解释器模式优缺点
- 优点
1、可扩展性比较好,灵活。
2、增加了新的解释表达式的方式。
3、易于实现文法。
- 缺点
1、执行效率比较低,可利用场景比较少。
2、对于复杂的文法比较难维护。
五、应用实例
用解释器模式设计一个搜索音乐的程序。
- 说明:
假如我们已知歌手名称和歌曲名称,获取播放歌曲,如果歌曲名称和歌手名称不匹配,返回暂时没有歌曲。
- 设计步骤
定义一个抽象表达式(Expression
)接口,它包含了解释方法 interpret(String info)
。
定义一个终结符表达式(Terminal Expression
)类,它用集合(Set)类来保存满足条件的歌手或歌曲名称,并实现抽象表达式接口中的解释方法 interpret(Stringinfo)
,用来判断被分析的字符串是否是集合中的终结符。
定义一个非终结符表达式(AndExpressicm
)类,它也是抽象表达式的子类,它包含满足条件的歌手的终结符表达式对象和满足条件的歌曲的终结符表达式对象,并实现 interpret(String info)
方法,用来判断被分析的字符串是否是满足条件的歌手中的满足条件的歌曲。
- 抽象表达式角色
public interface Expression {
public boolean interpret(String info);
}
- 终结符表达式 (TermianExpression)
public class TerminalExpression implements Expression {
// 存储歌曲名称和歌手
private Set<String> set= new HashSet<String>();
public TerminalExpression(String[] data){
for(int i=0;i<data.length;i++) set.add(data[i]);
}
@Override
public boolean interpret(String info) {
if(set.contains(info)){
return true;
}
return false;
}
}
- 非终结符表达式 (NonterminalExpression)
public class AndExpression implements Expression {
private Expression person;
private Expression songName;
public AndExpression(Expression person,Expression songName){
this.person=person;
this.songName=songName;
}
@Override
public boolean interpret(String info) {
String s[]=info.split("的");
return person.interpret(s[0])&&songName.interpret(s[1]);
}
}
- 环境角色
public class Context {
private String [] persons = {"薛之谦","刘德华","陈奕迅"};
private String [] songNames = {"丑八怪","冰雨","十年"};
private Expression personSong;
public Context(){
Expression person = new TerminalExpression(persons);
Expression songName = new TerminalExpression(songNames);
personSong = new AndExpression(person,songName);
}
public void findSongs(String info){
boolean ok = personSong.interpret(info);
if(ok) System.out.println("正在播放"+info+",这首歌曲!");
else System.out.println(info+",音乐播放器暂时没有这首音乐!");
}
}
- 测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.findSongs("陈奕迅的十年");
context.findSongs("刘德华的冰雨");
context.findSongs("薛之谦的丑八怪");
context.findSongs("周杰伦的青花瓷");
}
}
- 测试结果
正在播放陈奕迅的十年,这首歌曲!
正在播放刘德华的冰雨,这首歌曲!
正在播放薛之谦的丑八怪,这首歌曲!
周杰伦的青花瓷,音乐播放器暂时没有这首音乐!
六、模式的应用场景
1、当语言的文法较为简单,且执行效率不是关键问题是。
2、当问题重复出现,且可以用一种简单的语言来进行表达时。
3、当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。
- 模式的扩展
在项目开发中,如果要对数据表达式进行分析与计算,无须再用解释器模式进行设计了,Java
提供了以下强大的数学公式解析器:Expression4J
、MESP(Math Expression String Parser)
和 Jep
等,它们可以解释一些复杂的文法,功能强大,使用简单。
现在以 Jep
为例来介绍该工具包的使用方法。Jep
是 Java expression parser
的简称,即 Java
表达式分析器,它是一个用来转换和计算数学表达式的Java
库。通过这个程序库,用户可以以字符串的形式输入一个任意的公式,然后快速地计算出其结果。而且 Jep
支持用户自定义变量、常量和函数,它包括许多常用的数学函数和常量。
import com.singularsys.jep.*;
public class JepDemo{
public static void main(String[] args) throws JepException{
Jep jep=new Jep();
//定义要计算的数据表达式
String 存款利息="本金*利率*时间";
//给相关变量赋值
jep.addVariable("本金",10000);
jep.addVariable("利率",0.038);
jep.addVariable("时间",2);
jep.parse(存款利息); //解析表达式
Object accrual=jep.evaluate(); //计算
System.out.println("存款利息:"+accrual);
}
}
- 运行结果
存款利息:760.0
- 小结
解释器模式使用情况很少,使用时一定要结合业务进行判断是否符合这种设计模式。
GitHub地址:
https://github.com/xiaonongOne/interpreter-test/tree/master