本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-12-15
解释器模式(Interpreter):对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。
本文参考链接:参考1
场景:对机器人移动指令进行解释,移动的语法表达如下:方向 方式 距离,方向包括上下左右四个方向,方式包括跑以及一般移动,距离为一个整数,一条移动指令可以组合多条子移动指令,使用解释器模式进行设计。
// AbstractExpression(抽象表达式):声明了抽象的解释操作,是所有终结符表达式以及非终结符表达式的父类
class AbstractExpression {
constructor() {
if (new.target === AbstractExpression) {
throw new Error("抽象类不能被实例化");
}
}
interpret() {
if (this !== AbstractExpression) {
throw new Error("抽象方法必须由子类重写实现");
}
}
}
// TerminalExpression (终结符表达式)
// 抽象表达式的子类,实现了与文法规则中的终结符相关联的解释操作
// 句子中的每一个终结符都是该类的一个实例,通常只有少数几个终结符表达式类
// 处理方向的解释器
class DirectionNode extends AbstractExpression {
directMap = new Map([
['up', '向上'],
['down', '向下'],
['left', '向左'],
['right', '向右'],
]);
// ctx: Context 用于存储解释器之外的一些全局信息,通常它临时存储需要解释的语句
interpret(ctx) {
return this.directMap.has(ctx) ? this.directMap.get(ctx) : "无效操作"
}
}
// 处理动作的解释器
class ActionNode extends AbstractExpression {
actionMap = new Map([
['move', '移动'],
['run', '奔跑'],
])
interpret(ctx) {
return this.actionMap.has(ctx) ? this.actionMap.get(ctx) : "无效操作"
}
}
// 处理距离的解释器
class DistanceNode extends AbstractExpression {
interpret(ctx) {
return String(ctx);
}
}
// NonterminalExpression(非终结符表达式):也是抽象表达式的子类,实现了文法规则中非终结符的解释操作
// 由于非终结符表达式可以包含非终结符表达式以及终结符表达式,因此一般通过递归方式完成解释
// 处理语句的解释器,多个终结表达式组成一个非终结表达式,有点类似组合模式中 叶子对象和组合对象的关系
class SentenceNode extends AbstractExpression {
direction = new DirectionNode();
action = new ActionNode();
distance = new DistanceNode();
interpret(ctx) {
const [ dir, a, dis ] = ctx.split(' ');
// 将语句按照既定规则拆分给对应的“解释器”去“解释”
return this.direction.interpret(dir) + this.action.interpret(a) + this.distance.interpret(dis);
}
}
// 处理复合语句and的解释器
class AndNode extends AbstractExpression {
/**
* 解析器的实现思路
* 1.把客户端传递来的表达式进行分解,分解成为一个一个的元素,并用一个对应的解析模型来封装这个元素的一些信息。
* 2.根据每个元素的信息,转化成相对应的解析器对象。
* 3.按照先后顺序,把这些解析器对象组合起来,就得到抽象语法树了。
*
* 为什么不把1和2合并,直接分解出一个元素就转换成相应的解析器对象?
* 1.功能分离,不要让一个方法的功能过于复杂。
* 2.为了今后的修改和扩展,现在语法简单,所以转换成解析器对象需要考虑的东西少,直接转换也不难,但要是语法复杂了,直接转换就很杂乱了。
*/
interpret(ctx) {
if (ctx.includes('and')) { // 如果有and这个标识,则将其分割为两个语句递归处理
let idx = ctx.indexOf('and');
let leftSentence = ctx.slice(0, idx - 1);
let rightSentence = ctx.slice(idx + 4);
// 左、右边的句子应该使用何种“解释器”来“解释”
let leftExpr = leftSentence.includes('and') ? new AndNode() : new SentenceNode();
let rightExpr = rightSentence.includes('and') ? new AndNode() : new SentenceNode();
// 使用合适的“解释器”处理对应的句子(上下文),并按照既定规则进行合并“解释”
return leftExpr.interpret(leftSentence) + "再" + rightExpr.interpret(rightSentence);
}
// 如果不是复合语句,只是普通语句 “方向 + 动作 + 距离”,直接让SentenceNode解释器来解释
return new SentenceNode().interpret(ctx);
}
}
var complex = new AndNode();
const val = complex.interpret("up move 5 and down run 10 and down move 10 and left run -9")
console.log(val); // 向上移动5再向下奔跑10再向下移动10再向左奔跑-9
1.解释器模式功能:
解释器模式使用解释器对象来表示和处理相应的语法规则,一般一个解释器处理一条语法规则。理论上来说,只要能用解释器对象把符合语法的表达式表示出来,而且能够构成抽象的语法树,就可以使用解释器模式来处理。
2.语法规则和解释器
语法规则和解释器之间是有对应关系的,一般一个解释器处理一条语法规则,但是反过来并不成立,一条语法规则是可以有多种解释和处理的,也就是一条语法规则(如SentenceNode)可以对应多个解释器。
3.上下文的公用性
上下文在解释器模式中起着非常重要的作用。由于上下文会被传递到所有的解释器中。因此可以在上下文中存储和访问解释器的状态,比如,前面的解释器可以存储一些数据在上下文中,后面的解释器就可以获取这些值。
另外还可以通过上下文传递一些在解释器外部,但是解释器需要的数据,也可以是一些全局的,公共的数据。
上下文还有一个功能,就是可以提供所有解释器对象的公共功能,类似于对象组合,而不是使用继承来获取公共功能,在每个解释器对象中都可以调用。
4.谁来构建抽象语法树
在前面的示例中,是自己在客户端手工构建抽象语法树,是很麻烦的,(比如在AndNode使用“再”进行连接复合语句,这就是语法)但是在解释器模式中,并没有涉及这部分功能,只是负责对构建好的抽象语法树进行解释处理。可以提供解析器来实现把表达式转换成为抽象语法树。
还有一个问题,就是一条语法规则是可以对应多个解释器对象的,也就是说同一个元素,是可以转换成多个解释器对象的,这也就意味着同样一个表达式,是可以构成不同的抽象语法树的,这也造成构建抽象语法树变得很困难,而且工作量非常大。
5.谁负责解释操作
只要定义好了抽象语法树,肯定是解释器来负责解释执行。虽然有不同的语法规则,但是解释器不负责选择究竟用哪个解释器对象来解释执行语法规则,选择解释器的功能在构建抽象语法树的时候就完成了。
6.解释器模式的调用顺序
创建上下文对象。
创建多个解释器对象,组合抽象语法树。
调用解释器对象的解释操作。
通过上下文来存储和访问解释器的状态。
对于非终结符解释器对象,递归调用它所包含的终结符解释器对象。
解释器模式的本质:分离实现,解释执行。
解释器模使用一个解释器对象处理一个语法规则的方式,把复杂的功能分离开;然后选择需要被执行的功能,并把这些功能组合成为需要被解释执行的抽象语法树;再按照抽象语法树来解释执行,实现相应的功能。
从表面上看,解释器模式关注的是我们平时不太用到的自定义语法的处理;但从实质上看,解释器模式的思想然后是分离,封装,简化,和很多模式是一样的。