行为型设计模式之自定义表达式和解析可用解释器模式
1 定义
解释器模式(Interpreter Pattern)属于行为型设计模式,它主要是为了解决当出现一种特定类型的问题且发生频率足够高时,那么就可以将该类问题的各个实现表述为一个简单的语言句子,然后通过构造一个解释器来解释这些句子来解决问题。这是一种使用了编译语言的思想来分析应用中的实例的行为。通俗些说就是提供一种有语法规则的表达式,然后再设计出一个解释器来解释该表达式的意思。例如现在我们需要实现一个非阿拉伯数字而是可以识别汉字的计算器程序,当传入如“伍加拾壹加捌减六”便可像传入“5+11+8-6”一样计算出结果为18,那么“伍加拾壹加捌减六”便是上面所说的语言句子或者说是表达式,我们需要设计出一个解释器来解释此类表达式所表达的意思然后进行转换计算。
2 相关概念
在开始实现前,我们先来理解解释器模式中将会用到的两个的概念:终结符 和 非终结符。它们常出现在编译原理知识里,是用来指定推导规则的元素。
终结符(T):就是不能再进行拆分的最小元素。
非终结符(N): 就是可再拆分的元素,是一个递归形式的定义,由终结符和至少一个非终结符号组成的元素。
还是很抽象吗?没有关系,这毕竟是编译原理里的概念,我们只需要知道在解释器模式中,自定义的语言句子表达式里,哪些属于终结符,哪些属于非终结符就可以了。听完下面的解释你解释一定会清晰很多。
比如上述举例表达式“伍加拾壹加捌减六”中,“伍”、“拾壹”、“捌”和“六”便是终结符,像“拾壹”虽然是两个字,但它已经是最小的元素不可能再进行拆分了,所以它也是终结符。
而“伍加拾壹”便是一个非终结符,因为“加”和“减”充当着一个规则关键字符号用于将两个终结符进行连接成一个非终结符,这里就是终结符“伍”和一个规则关键字符号“加”以及另外一个终结符“拾壹”组成。同理在“伍加拾壹”成为一个非终结符后再和“加”、“捌”又组成另一个非终结符。
3 实现
解释器模式一般包含4种角色,分别是:
- 抽象表达式(AbstractExpression):用于定义解释器的接口,一般包含解释方法interpret。
- 终结符表达式(TerminalExpression):实现抽象表达式的解释方法,用于实现表达式中与终结符相关的操作,通常只有一个终结表达式类,但有着多个对应着不同终结符的对象。例如上述定义举例的“伍”、“拾壹”、“捌”和“六”。
- 非终结符表达式(NonterminalExpression): 实现抽象表达式的解释方法,用于实现表达式中与非终结符相关的操作,原则上每个规则关键字符号对应一个非终结符表达式类,类中持着两个表达式对象。
- 环境上下文(Context):包含解释器中需要的数据或公共功能,一般用来传递共享数据。
抽象表达式类,只定义一个解释器抽象方法:
public abstract class ArithmeticExpression {
public abstract int interpret();
}
终结符表达式类,这里是一个数字终结符表达式,用于将汉字转换成可计算的数字(示例只支持最大数值为12):
public class NumExpression extends ArithmeticExpression {
private String mName;
public NumExpression(String name) {
mName = name;
}
public String getName() {
return mName;
}
@Override
public int interpret() {
switch (mName) {
case "一":
case "壹":
return 1;
case "二":
case "贰":
return 2;
case "三":
case "叁":
return 3;
case "四":
case "肆":
return 4;
case "五":
case "伍":
return 5;
case "六":
case "陆":
return 6;
case "七":
case "柒":
return 7;
case "八":
case "捌":
return 8;
case "九":
case "玖":
return 9;
case "十":
case "拾":
return 10;
case "十一":
case "拾壹":
return 11;
case "十二":
case "拾贰":
return 12;
// ……暂只支持12,需要自行扩展
default:
return 0;
}
}
}
非终结符表达式类,这里因为存在两个规则关键字符号:加和减,所以会有两个非终结符表达式类,而为了抽象公共代码,所以又有了一个抽象运算非终结符表达式类:
public abstract class OperatorExpression extends ArithmeticExpression {
protected ArithmeticExpression mArithmeticExpression1, mArithmeticExpression2;
public OperatorExpression(ArithmeticExpression arithmeticExpression1, ArithmeticExpression arithmeticExpression2) {
mArithmeticExpression1 = arithmeticExpression1;
mArithmeticExpression2 = arithmeticExpression2;
}
}
public class AdditionExpression extends OperatorExpression {
public AdditionExpression(ArithmeticExpression arithmeticExpression1, ArithmeticExpression arithmeticExpression2) {
super(arithmeticExpression1, arithmeticExpression2);
}
@Override
public int interpret() {
return mArithmeticExpression1.interpret() + mArithmeticExpression2.interpret();
}
}
public class SubtractExpression extends OperatorExpression {
public SubtractExpression(ArithmeticExpression arithmeticExpression1, ArithmeticExpression arithmeticExpression2) {
super(arithmeticExpression1, arithmeticExpression2);
}
@Override
public int interpret() {
return mArithmeticExpression1.interpret() - mArithmeticExpression2.interpret();
}
}
环境上下文类,它内部持有着一个后入先出的Stack队列,而构造函数中接收表达式字符串,并将表达式进行分解成数字和规则关键字元素列表,接着循环元素列表进行终结符表达式和非终结符表达式的装载到Stack队列:
public class Calculator {
// 用Stack存储并操作所有相关的解释器,其特点是后入先出
protected Stack<ArithmeticExpression> mArithmeticExpressionStack = new Stack<>();
public Calculator(String expression) {
// 运算符左右两边的数字解释器
ArithmeticExpression arithmeticExpression1;
ArithmeticExpression arithmeticExpression2;
// 分解表达式
List<String> elementList = new ArrayList<>();
String[] addElements = expression.split("加");
for (int i = 0; i < addElements.length; i++) {
String[] subtractElements = addElements[i].split("减");
for (int j = 0; j < subtractElements.length; j++) {
elementList.add(subtractElements[j]);
if (j != subtractElements.length - 1) {
elementList.add("减");
}
}
if (i != addElements.length - 1) {
elementList.add("加");
}
}
for (int i = 0; i < elementList.size(); ++i) {
switch (elementList.get(i)) {
case "加": {
// 取出栈顶的解释器(上一次刚添加入栈的数字解释器),来作为运算符左边的解释器
arithmeticExpression1 = mArithmeticExpressionStack.pop();
// 将运算符右边元素构造为一个数字解析器
arithmeticExpression2 = new NumExpression(elementList.get(++i));
// 传入运算符左右两边的解析器到加/减法运算解释器中
ArithmeticExpression arithmeticExpression = new AdditionExpression(arithmeticExpression1, arithmeticExpression2);
// 将运算解释器添加入栈
mArithmeticExpressionStack.push(arithmeticExpression);
break;
}
case "减": {
// 跟加同理
arithmeticExpression1 = mArithmeticExpressionStack.pop();
arithmeticExpression2 = new NumExpression(elementList.get(++i));
ArithmeticExpression arithmeticExpression = new SubtractExpression(arithmeticExpression1, arithmeticExpression2);
mArithmeticExpressionStack.push(arithmeticExpression);
break;
}
default:
// 如果是数字,直接构造数字解释器并添加入栈
mArithmeticExpressionStack.push(new NumExpression(elementList.get(i)));
break;
}
}
}
public int calculate() {
return mArithmeticExpressionStack.pop().interpret();
}
}
将表达式字符串先转成元素列表,元素列表的最终值如下图:
装载所有元素到Stack队列后,Stack队列的最终值如下图:
- 最内部黄色两个框是数字终结符表达式“伍”对象和数字终结符表达式“拾壹”对象,它们通过规则关键字符“加”相连后形成了一个加法非终结符表达式,即蓝框对象;
- 上面的蓝框对象也就是加法非终结符表达式对象再跟另一个数字终结符表达式“捌”对象通过规则关键字符“加”相连后形成一个新的加法非终结符表达式,即红框对象;
- 上面的红框对象也就是新的加法非终结符表达式 对象再跟另一个数字终结符表达式“六”对象通过规则关键字符“减”相连后形成一个新的最终的减法非终结符表达式。
客户端:
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator("伍加拾壹加捌减六");
int result = calculator.calculate();
System.out.println("伍加拾壹加捌减六 等于:" + result);
}
}
输出结果:
伍加拾壹加捌减六 等于:18
4 总结
解释器模式是较为冷门的模式,本质就是遇到发生频率较高的重复问题上,将实现表述成一个句子表达式,然后将句子表达式模块化,通过分离语法和解释语法来解决问题。如在一些云端配置库中的某些关键且简单的匹配表达式可使用到,总体上可应用的场景并不多。因为它执行效率并不高,在解释过程中如果表达式句子元素过多的情况时会使用到大量的循环和递归,如果表达式句子规则过多的情况时又会引起类的增多,所以在性能和调试上也需要一定的成本。当然灵活性好是该模式的最大优点,增加规则只需要通过增加非终结符表达式类来完成而且实现思想清晰简单。
本文地址:https://blog.csdn.net/lyz_zyx/article/details/109485261
上一篇: 设计模式详解—中介者模式
下一篇: 【Java5 特性】3.Java枚举