假设系统的一个文本框中,允许用户输入字符串表达式如"5除 2 乘3模4乘6",要求系统能够按照Java的整数乘除运算规则,计算出表达式结果,如12。
假设系统的一个文本框中,允许用户输入字符串表达式如“list 姓名 年龄 学号 sortby 学号”, 要求系统能够从数据库中提取学生信息并按照学号排序。
当应用程序中频繁使用某种文本形式的句子(不管是用户输入的还是从文件中读取的)时,我们需要为定义字符串表达式的语言设计一个解释器(interpreter)。
【在实际的应用程序开发中,编写一个解释器的机会较少,而且编写复杂的解释器需要学习《编译原理》。因而yqj2065认为解释器模式(Interpreter Pattern) 是所有设计模式中最难学习的。但是,如果你大致了解了《编译原理》方面的知识,则学习解释器模式的难度系数,从7颗星直接降到4星。所以,下一节的内容难度也就4星。】
6.3.1 乘法解释器
本节以一种极其简化的表达式语言——乘法/Mul语言为例,介绍该语言解释器的设计。
Mul语言,描述“字符串”形式书写的表示乘除法的各种句子,如"5.0 / 2 * 4 % 3 * 6"(简化起见,不考虑优先级和括号的使用),要求该语句(或表达式)能够按照Java的double数据的乘除运算规则,计算出结果。
1. 语法元素
作为一门语言,Mul语言有一套规则,或称为文法/语法/Grammars。回顾《编译原理》介绍的知识,通常,文法由四个元素构成,表示为一个四元组:G = {ST,SN,S,P}。
①终结符(terminal)的集合ST(set of terminal)。终结符是语言的最小文法元素(token/语言符号)。Mul语言的终结符只考虑数字和3个操作符,暂时不考虑括号和空格。ST的集合表示为:{ Digit,*, /,%}。
②非终结符(语法变量)的集合SN(set of nonterminal)。非终结符是由终结符构成的表达式片段,Java的非终结符包括表达式、语句(if块)、函数、类等大量的语法概念;而Mul语言的非终结符仅仅只有表达式。
③产生式集合P。产生式定义什么是合法的非终结符,产生式格式为“α→β”,意为α定义为β。Expr类层次为每一种产生式规则定义一个类。
Exp→Digit| Digit op Digit | Exp op Exp
其中,op={* | / | %}。
④开始符号S。S是每个合法句子的最开始处使用的非终结符,为Digit。
上述四元组,可以用表达式的递归定义描述。Mul语言的表达式:
- 数字是一个表达式;
- 中缀操作符连接两个表达式是一个表达式;
因此在设计解释器的实现时,按照表达式的递归定义,首先设计接口Expr,它代表/封装表达式,它需要计算出表达式的结果,封装了一个eval()方法,对本表达式(Expr实例)进行解释(计算) 并返回计算的结果。
其次为3种操作的中缀表示,设计一个父类型Op,其子类Mul (Expr left,Expr right)描述乘法操作符*组成的表达式。Op可以不作为Expr的子类型,那么Mul需要多继承。
Digit封装字符串形式的数字,Digit对象必须是Double.parseDouble(value)能够解析的value。
例程 6 8封装文法
package chap6.interpreter.MulLang;
public interface Expr {//Expression
public double eval();
}
abstract class Op implements Expr { // Op可以不作为Expr的子类型
protected final Expr left, right;//
public Op(Expr left, Expr right) {
this.left = left;
this.right = right;
}
}
class Mul extends Op{
public Mul(Expr left,Expr right){super(left,right); }
@Override public double eval() {return left.eval() * right.eval();}
}// Div 、 Mod略
class Digit implements Expr {
private final double value;
public Digit(String value) {
this.value = Double.parseDouble(value);
}
@Override public double eval() { return this.value; }
}
Expr类层次描述了Mul语言的规则。Expr类层次为每一种产生式规则定义一个类。即
Exp→Digit| Digit op Digit | Exp op Exp
其中,Op ={Mul | Div | Mod}。
2. 抽象语法树
合法的表达式如"5.0 / 2 * 4 % 3 * 6",可以手工创建其Expr对象。这样纯手工的玩意,看上去蛮有趣和亲切的——因为傻傻的。
Expr expr = new Mul(
new Mod(
new Mul(
new Div(new Digit("5.0"), new Digit("2") ),
new Digit("4")),
new Digit("3")),
new Digit("6"));
expr.eval()的值为6.0。Expr对象可以用树形结构来描述,称为抽象语法树(Abstract Syntax Tree, AST)或简称语法树。手工创建Expr对象极其繁琐,通常使用栈构造语法树。(单纯看Expr与Digit和Op的类图,和组合模式、装饰模式完全一样的结构。区别和内在联系后面讨论。)
例程 7 4构造语法树
public static Expr build(String statement){
statement = statement.replace(" ", "");
statement = statement.replace("*", " * ").replace("/", " / ").replace("%", " % ");
String[] tokens=statement.split("\\s+");//"\\s+"
Expr left=null,right=null;
Expr expr=null;
Stack<Expr> stack=new Stack<>();
for(int i=0;i<tokens.length;i++){
if(tokens[i].equals("*")){
left= stack.pop();
right=new Digit(tokens[++i]);
stack.push(new Mul(left,right));
} else if(tokens[i].equals("/")){
left= stack.pop();
right=new Digit(tokens[++i]);
stack.push(new Div(left,right));
}else if(tokens[i].equals("%")){
left= stack.pop();
right=new Digit(tokens[++i]);
stack.push(new Mod(left,right));
}else {
stack.push(new Digit(tokens[i]));
}
}
return expr=(Expr)stack.pop();
}
public class Client{
public static void main(String args[]) {
String statement = "6 / 2 * 3 % 4 * 6";
statement = "62 / 3";
Calculator calculator = new Calculator();
calculator.build(statement);
int result = calculator.compute();
System.out.println(statement + " = " + result);
}
public static void test() {
Expr e = new DivOp(new Digit(62),new Digit(3));
int result = e.interpret();
System.out.println(" 62 / 3 = " + result);
}
}
[设计模式]中的解释器模式并未解释如何创建一个抽象的语法树。
方法build(String statement)将参数statement解析并构造一个Expr实例。