简说设计模式——解释器模式
一、什么是解释器模式
解释器这个名词想必大家都不会陌生,比如编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。诸如此类的例子也有很多,比如编译器、正则表达式等等。
如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子,这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
就比如正则表达式,它就是解释器模型的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。
解释器模式(Interpreter),给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。UML结构图如下:
其中,Context是环境角色,包含解释器之外的一些全局信息;AbstractExpression为抽象表达式,声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享;TerminalExression为终结符表达式,实现与文法中的终结符相关联的解释操作;NonterminalExpression为非终结符表达式,为文法中的非终结符实现解释操作,对文法中每一条规则R1、R2……Rn都需要一个具体的非终结符表达式类。
1. Context环境角色
1 public class Context { 2 3 private String input; 4 private String output; 5 6 public String getInput() { 7 return input; 8 } 9 public void setInput(String input) {10 this.input = input;11 }12 public String getOutput() {13 return output;14 }15 public void setOutput(String output) {16 this.output = output;17 }18 19 }
2. 抽象表达式
抽象表达式是生成语法集合(语法树)的关键,每个语法集合完成指定语法解析任务,它是通过递归调用的方式,最终由最小的语法单元进行解析完成。
1 public abstract class AbstractExpression {2 public abstract void Interpret(Context context);3 }
3. 终结符表达式
通常,终结符表达式比较简单,主要处理场景元素和数据的转换。
1 public class TerminalExpression extends AbstractExpression {2 3 @Override4 public void Interpret(Context context) {5 System.out.println("终端解释器");6 }7 8 }
4. 非终结符表达式
每个非终结符表达式都代表了一个文法规则,并且每个文法规则都只关心自己周边的文法规则的结果,因此这就产生了每个非终结符表达式调用自己周边的非终结符表达式,然后最终、最小的文法规则就是终结符表达式。
1 public class NonterminalExpression extends AbstractExpression {2 3 @Override4 public void Interpret(Context context) {5 System.out.println("非终端解释器");6 }7 8 }
5. Client客户端
其中list为一个语法容器,容纳一个具体的表达式。通常Client是一个封装类,封装的结果就是传递进来一个规范语法文件,解析器分析后产生结果并返回,避免了调用者与语法分析器的耦合关系。
1 public class Client { 2 3 public static void main(String[] args) { 4 Context context = new Context(); 5 List<AbstractExpression> list = new ArrayList<>(); 6 7 list.add(new TerminalExpression()); 8 list.add(new NonterminalExpression()); 9 list.add(new TerminalExpression());10 list.add(new TerminalExpression());11 12 for (AbstractExpression abstractExpression : list) {13 abstractExpression.Interpret(context);14 }15 }16 17 }
运行结果如下:
二、解释器模式的应用
1. 何时使用
当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时
2. 方法
构建语法树,定义终结符与非终结符
3. 优点
可扩展性好
4. 缺点
解释器模式会引起类膨胀
解释器模式采用递归调用方法,将会导致调试非常复杂
使用了大量的循环和递归,效率是一个不容忽视的问题
5. 使用场景
可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
一些重复出现的问题可以用一种简单的语言来表达
一个简单语法需要解释的场景
6. 应用实例
编译器
运算表达式计算、正则表达式
机器人
7. 注意事项
尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题
三、解释器模式的实现
我们现在通过解释器模式来实现四则运算,如计算a+b的值。UML图如下:
1. 解析器封装类
使用Calculator构造函数传参,并解析封装。这里根据栈的“先进后出”来安排运算的先后顺序(主要用在乘除法,这里只写了加减法比较简单)。以加法为例,Calculator构造函数接收一个表达式,然后把表达式转换为char数组,并判断运算符号,如果是'+’则进行加法运算,把左边的数(left变量)和右边的数(right变量)加起来即可。
例如a+b-c这个表达式,根据for循环,首先被压入栈中的是a元素生成的VarExpression对象,然后判断到加号时,把a元素的对象从栈中pop出来,与右边的数组b进行相加,而b是通过当前的数组游标下移一个单元格得来的(为了防止该元素被再次遍历,通过++i的方式跳过下一遍历)。减法运算同理。
1 public class Calculator { 2 3 //定义表达式 4 private Expression expression; 5 6 //构造函数传参,并解析 7 public Calculator(String expStr) { 8 //安排运算先后顺序 9 Stack<Expression> stack = new Stack<>();10 //表达式拆分为字符数组11 char[] charArray = expStr.toCharArray();12 13 Expression left = null;14 Expression right = null;15 for(int i=0; i<charArray.length; i++) {16 switch (charArray[i]) {17 case '+': //加法18 left = stack.pop();19 right = new VarExpression(String.valueOf(charArray[++i]));20 stack.push(new AddExpression(left, right));21 break;22 case '-': //减法23 left = stack.pop();24 right = new VarExpression(String.valueOf(charArray[++i]));25 stack.push(new SubExpression(left, right));26 break;27 default: //公式中的变量28 stack.push(new VarExpression(String.valueOf(charArray[i])));29 break;30 }31 }32 this.expression = stack.pop();33 }34 35 //计算36 public int run(HashMap<String, Integer> var) {37 return this.expression.interpreter(var);38 }39 40 }
2. 抽象表达式类
通过Map键值对,使键对应公式参数,如a、b、c等,值为运算时取得的具体数值。
1 public abstract class Expression {2 3 //解析公式和数值,key是公式中的参数,value是具体的数值4 public abstract int interpreter(HashMap<String, Integer> var);5 6 }
3. 变量解析器
通过interpreter()方法从map中取之。
1 public class VarExpression extends Expression { 2 3 private String key; 4 5 public VarExpression(String key) { 6 this.key = key; 7 } 8 9 @Override10 public int interpreter(HashMap<String, Integer> var) {11 return var.get(this.key);12 }13 14 }
4. 抽象运算符号解析器
这里,每个运算符合都只和自己左右两个数字有关系,但左右两个数字有可能也是一个解析的结果,无论何种类型,都是Expression类的实现类。
1 public class SymbolExpression extends Expression { 2 3 protected Expression left; 4 protected Expression right; 5 6 public SymbolExpression(Expression left, Expression right) { 7 this.left = left; 8 this.right = right; 9 }10 11 @Override12 public int interpreter(HashMap<String, Integer> var) {13 // TODO Auto-generated method stub14 return 0;15 }16 17 }
5. 加法解析器
1 public class AddExpression extends SymbolExpression { 2 3 public AddExpression(Expression left, Expression right) { 4 super(left, right); 5 } 6 7 public int interpreter(HashMap<String, Integer> var) { 8 return super.left.interpreter(var) + super.right.interpreter(var); 9 }10 11 }
6. 减法解析器
1 public class SubExpression extends SymbolExpression { 2 3 public SubExpression(Expression left, Expression right) { 4 super(left, right); 5 } 6 7 public int interpreter(HashMap<String, Integer> var) { 8 return super.left.interpreter(var) - super.right.interpreter(var); 9 }10 11 }
7. Client客户端
这里就比较简单了,通过getExpStr()方法获取表达式,再通过getValue()方法获取值的映射,最后再实例化Calculator类,通过run()方法获取最终的运算结果。
1 public class Client { 2 3 public static void main(String[] args) throws IOException { 4 String expStr = getExpStr(); 5 HashMap<String, Integer> var = getValue(expStr); 6 Calculator calculator = new Calculator(expStr); 7 System.out.println("运算结果:" + expStr + "=" + calculator.run(var)); 8 } 9 10 //获得表达式11 public static String getExpStr() throws IOException {12 System.out.print("请输入表达式:");13 return (new BufferedReader(new InputStreamReader(System.in))).readLine();14 }15 16 //获得值映射17 public static HashMap<String, Integer> getValue(String expStr) throws IOException {18 HashMap<String, Integer> map = new HashMap<>();19 20 for(char ch : expStr.toCharArray()) {21 if(ch != '+' && ch != '-' ) {22 if(! map.containsKey(String.valueOf(ch))) {23 System.out.print("请输入" + String.valueOf(ch) + "的值:");24 String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();25 map.put(String.valueOf(ch), Integer.valueOf(in));26 }27 }28 }29 30 return map;31 }32 33 }
运算结果如下: