17.解释器模式(行为型)

2019-09-15  本文已影响0人  哈哈大圣

解释器模式(行为型)

解释器模式很难学,使用率很低!

一、相关概念

1). 解释器模式概述

解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言。对解释器模式的学习能够加深我们对面向对象思想的理解,并且掌握编程语言中文法规则的解释过程。

  1. 解释器模式:定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。

  2. 结构图

由于表达式可分为终结符表达式和非终结符表达式,因此解释器模式的结构与组合模式的结构有些类似,但在解释器模式中包含更多的组成元素


解释器模式.png

2). 相关角色

  1. AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
  2. TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
  3. NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
  4. Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。

3). 典型代码

  1. 抽象表达式
import java.util.HashMap;

/**
 * 抽象表达式
 */
abstract class AbstractExpression {
    public abstract void interpret(Context context);
}

在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类

  1. 终结符表达式
/**
 * 终结符表达式
 */
class TerminalExpression extends AbstractExpression {

    @Override
    public void interpret(Context context) {

    }
}

终结符表达式和非终结符表达式类都是抽象表达式类的子类,对于终结符表达式,其代码很简单,主要是对终结符元素的处理。

  1. 非终结符表达式
/**
 * 非终结符表达式
 */
class NonterminalExpression extends AbstractExpression {
    private AbstractExpression left;
    private AbstractExpression right;

    public NonterminalExpression(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public void interpret(Context context) {
        // 递归调用每一个组成部分的interpret方法
        // 在递归调用时指定组成部分的连接方式,即非终结符的功能。
    }
}

对于非终结符表达式,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类。

  1. 上下文环境
/**
 * 上下文环境类
 */
class Context {
    private HashMap<String, String> map = new HashMap<String, String>();

    public void assign(String key, String value) {
        // 往环境中设值
        map.put(key, value);
    }

    public String lookup(String key) {
        // 获取存储在环境类中的值
        return map.get(key);
    }
}

除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个环境类Context,用于存储一些全局信息,通常在Context中包含了一个HashMap或ArrayList等类型的集合对象(也可以直接由HashMap等集合类充当环境类),存储一系列公共信息,如变量名与值的映射关系(key/value)等,用于在进行具体的解释操作时从中获取相关信息。

当系统无须提供全局公共信息时可以省略环境类,可根据实际情况决定是否需要环境类。

二、解释器模式设计步骤

1). 案例需求描述

提供一个简单的加法/减法解释器,只要输入一个加法/减法表达式,它就能够计算出表达式结果

2). 解析为文法规则和抽象语法树

  1. 文法规则
expression ::= value | operation
operation ::= expression '+' expression | expression '-' expression
value ::= an integer //一个整数值

在解释器模式中每一个文法规则语句都将对应一个类,扩展、改变文法以及增加新的文法规则都很方便。

该文法规则包含三条语句,第一条表示表达式的组成方式,其中value和operation是后面两个语言单位的定义,每一条语句所定义的字符串如operation和value称为语言构造成分或语言单位,符号“::=”表示“定义为”的意思,其左边的语言单位通过右边来进行说明和定义,语言单位对应终结符表达式和非终结符表达式。如本规则中的operation是非终结符表达式,它的组成元素仍然可以是表达式,可以进一步分解,而value是终结符表达式,它的组成元素是最基本的语言单位,不能再进行分解。

在文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“boolValue ::= 0 | 1”表示终结符表达式boolValue的取值可以为0或者1。

  1. 抽象语法树

除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树(Abstract Syntax Tree, AST)的图形方式来直观地表示语言的构成,每一棵抽象语法树对应一个语言实例,如加法/减法表达式语言中的语句“1+ 2 + 3 – 4 + 1”,可以通过如图18-2所示抽象语法树来表示:


加法抽象语法树.png

在该抽象语法树中,可以通过终结符表达式value和非终结符表达式operation组成复杂的语句,每个文法规则的语言实例都可以表示为一个抽象语法树,即每一条具体的语句都可以用类似图18-2所示的抽象语法树来表示,在图中终结符表达式类的实例作为树的叶子节点,而非终结符表达式类的实例作为非叶子节点,它们可以将终结符表达式类的实例以及包含终结符和非终结符实例的子表达式作为其子节点。抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符类和非终结符类。

3). 代码实现

  1. 抽象表达式

import java.util.Stack;

/**
 * 抽象表达式
 */
interface CalculateNode {
    public abstract double interpret();
}

  1. 非终结表达式
/**
 * 非终结符表达式:符号解释
 */
class SymbolNode implements CalculateNode {

    // SymbolNode的左表达式
    private CalculateNode left;

    // SymbolNode的右表达式
    private CalculateNode right;

    // Symbol符号 对应 + -
    private String symbol;

    public SymbolNode(CalculateNode left, CalculateNode right, String symbol) {
        this.left = left;
        this.right = right;
        this.symbol = symbol;
    }

    // 符号表达式解释操作
    @Override
    public double interpret() {
        switch (this.symbol) {
            case "+" : return left.interpret() + right.interpret();
            case "-" : return left.interpret() - right.interpret();
            default: throw new RuntimeException("操作符格式不正确");
        }
    }
}
  1. 终结表达式
/**
 * 终结表达式:数字类
 */
class NumberNode implements CalculateNode {
    private String number;

    public NumberNode(String number) {
        this.number = number;
    }

    // 简单句子的解释操作
    @Override
    public double interpret() {
        return Double.parseDouble(this.number);
    }
}

  1. 指令处理器
/**
 * 指令处理器:工具类
 */
class InstructionHandlerForNumber {

    private String instruciton;
    private CalculateNode node;

    public void handle(String instruciton) {

        this.instruciton = instruciton;

        CalculateNode left = null;
        CalculateNode right = null;

        // 声明一个栈对象用于存储抽象语法树
        Stack<CalculateNode> stack = new Stack<>();

        // 以空格分隔指令字符串
        String[] words = instruciton.split(" ");

        // 将第一个数字入栈
        stack.push(new NumberNode(words[0]));

        // 从第二个开始解析
        for (int i = 1; i < words.length;) {

            // 弹出栈顶表达式作为左语句
            left = stack.pop();
            String symbol = words[i++];
            right = new NumberNode(words[i++]);

            stack.push(new SymbolNode(left, right, symbol));

        }

        // 将整个解析的指令全部取出
        this.node = stack.pop();
    }

    public double output() {
        return node.interpret();
    }
}
  1. 客户端测试
/**
 * @author Liucheng
 * @since 2019-09-03
 */
public class ClientThree {

    public static void main(String[] args) {
        String instruciton = "1 + 6 - 2 + 3";
        InstructionHandlerForNumber handler = new InstructionHandlerForNumber();
        handler.handle(instruciton);
        double output = handler.output();
        System.out.println(output);
    }
}

三、复杂案例演示

1). 案例需求描述

Sunny软件公司欲为某玩具公司开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up)、下(down)、左(left)、右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。例如:“down run 10 and left move 20” 可以解释为 “向下快速移动10个单位再向左移动20个单位”

2). 文法规则描述

  1. 文法规则描述
expression ::= direction action distance | composite //表达式
composite ::= expression 'and' expression //复合表达式
direction ::= 'up' | 'down' | 'left' | 'right' //移动方向
action ::= 'move' | 'run' //移动方式
distance ::= an integer //移动距离

上述语言一共定义了五条文法规则,对应五个语言单位(解释器模式中每一个文法规则都将对应一个类,扩展、改变文法以及增加新的文法规则都很方便),这些语言单位可以分为两类,一类为终结符(也称为终结符表达式),例如direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也称为非终结符表达式),例如expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。

  1. 抽象语法树举例
    “down run 10 and left move 20”

机器人抽象语法树.png

3). 类图设计


机器人类图.png

4). 完整代码

  1. 抽象表达式
package interpreterpattern;

import java.util.Stack;

/**
 * 抽象表达式
 */
abstract class AbstractNode {
    public abstract String interpret();
}
  1. 非终结符表达式
/**
 * 非终结符表达式:And解释
 */
class AndNode extends AbstractNode {
    // And的左表达式
    private AbstractNode left;
    // And的右表达式
    private AbstractNode right;

    public AndNode(AbstractNode left, AbstractNode right) {
        this.left = left;
        this.right = right;
    }

    // And表达式解释操作
    @Override
    public String interpret() {
        return left.interpret() + "再" + right.interpret();
    }
}

/**
 * 非终结符表达式:简单句子解释
 */
class SentenceNode extends AbstractNode {
    private AbstractNode direction;
    private AbstractNode action;
    private AbstractNode distance;

    public SentenceNode(AbstractNode direction, AbstractNode action, AbstractNode distance) {
        this.direction = direction;
        this.action = action;
        this.distance = distance;
    }

    // 简单句子的解释操作
    @Override
    public String interpret() {
        StringBuilder sb = new StringBuilder();
        sb.append(direction.interpret())
                .append(action.interpret())
                .append(distance.interpret());

        return sb.toString();
    }
}

  1. 终结符表达式
/**
 * 终结符表达式:方向类
 */
class DirectionNode extends AbstractNode {
    private String direction;

    public DirectionNode(String direction) {
        this.direction = direction;
    }

    // 方向表达式的解释操作
    @Override
    public String interpret() {
        switch (this.direction) {
            case "up": return "向上";
            case "down": return "向下";
            case "left": return "向左";
            case "right": return "向右";
            default:return "无效指令";
        }
    }
}

/**
 * 终结符表达式:动作解释
 */
class ActionNode extends AbstractNode {

    private String action;

    public ActionNode(String action) {
        this.action = action;
    }

    // 表达式解释
    @Override
    public String interpret() {
        switch (this.action) {
            case "move": return "移动";
            case "run": return "快速移动";
            default:return "无效指令";
        }
    }
}

/**
 * 终结符表达式;距离解释
 */
class DistanceNode extends AbstractNode {

    private String distance;

    public DistanceNode(String distance) {
        this.distance = distance;
    }

    // 距离解释
    @Override
    public String interpret() {
        return this.distance;
    }
}

  1. 指令处理器
/**
 * 指令处理器:工具类
 */
class InstructionHandler {
    private String instruciton;
    private AbstractNode node;

    public void handle(String instruciton) {
        AbstractNode left = null;
        AbstractNode right = null;

        AbstractNode direction = null;
        AbstractNode action = null;
        AbstractNode distance = null;

        // 声明一个栈对象用于存储抽象语法树
        Stack<AbstractNode> stack = new Stack<>();

        // 以空格分隔指令字符串
        String[] words = instruciton.split(" ");

        // 将前三个单词取出,解析并放入栈中
        AbstractNode firstNode = new SentenceNode(
                new DirectionNode(words[0]),
                new ActionNode(words[1]),
                new DistanceNode(words[2]));

        stack.push(firstNode);

        // 此处有个and连接词,将其屏蔽掉
        for (int i = 4; i < words.length; i++) {
            // 本实例采用栈的方式来处理指令,如果遇到"and",
            // 则将其后的三个单词作为三个终结符表达式

            String word1 = words[i++];
            direction = new DirectionNode(word1);

            String word2 = words[i++];
            action = new ActionNode(word2);

            String word3 = words[i++];
            distance = new DistanceNode(word3);

            // 弹出栈顶表达式作为左语句
            left = stack.pop();
            // 创建右语句
            right = new SentenceNode(direction, action, distance);

            // 将新表达式压入栈中
            stack.push(new AndNode(left, right));
        }

        // 将整个解析的指令全部取出
        this.node = stack.pop();
    }

    public String output() {
        return node.interpret();
    }
}
  1. 客户端测试类
/**
 * @author liucheng
 * @since 2019/9/3
 */
public class Client {
    public static void main(String[] args) {
        String instruction = "up move 5 and down run 10 and left move 3 and right run 2";
        InstructionHandler handler = new InstructionHandler();
        handler.handle(instruction);
        System.out.println(handler.output());
    }
}

四、Context的作用【难点*****】

在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据,此外还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。接下来以一个案例进行演示说明。

1). 案例需求描述

Sunny软件公司开发了一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面中输出一些格式化内容,例如输入LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉,将输出如下结果:

杨过 小龙女
杨过 小龙女
郭靖 黄蓉

其中关键词LOOP表示“循环”,后面的数字表示循环次数;PRINT表示“打印”,后面的字符串表示打印的内容;SPACE表示“空格”;BREAK表示“换行”;END表示“循环结束”。每一个关键词对应一条命令,计算机程序将根据关键词执行相应的处理操作。

2). 文法规则设计

expression ::= command* //表达式,一个表达式包含多条命令
command ::= loop | primitive //语句命令
loop ::= 'loopnumber' expression 'end' //循环命令,其中number为自然数
primitive ::= 'printstring' | 'space' | 'break' //基本命令,其中string为字符串

抽象语法数举例略

3). 类图设计

根据4条文法规则设计类图


context.png

Context充当环境角色,Node充当抽象表达式角色,ExpressionNode、CommandNode和LoopCommandNode充当非终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色。

4). 完整代码

  1. context环境类
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

/**
 * 环境类:用于存储和操作需要解释的语句,
 * 在本实例中每一个需要解释的单词可以称为一个
 */
class Context {
    // /StringTokenizer类,用于将字符串分解为更小
    private StringTokenizer tokenizer;

    // 当前字符串标记
    private String currentToken;

    public Context(String text) {
        // //通过传入的指令字符串创建StringTokenizer
        tokenizer = new StringTokenizer(text);
        nextToken();
    }

    // 返回下一个标记
    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken = null;
        }

        return currentToken;
    }

    // 返回当前的标记
    public String currentToken() {
        return this.currentToken;
    }

    // 跳过一个标记
    public void skipToken(String token) {
        if (!token.equals(currentToken)) {
            System.out.println("错误提示:" + currentToken + "解释错误!");
        }

        nextToken();
    }

    // 如果当前标记是一个数字,则返回对应的数值
    public int currentNumber() {
        int number = 0;
        try {
            // 将字符串转换为数字
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        return number;
    }
}

环境类Context类似一个工具类,它提供了用于处理指令的方法,如nextToken()、currentToken()、skipToken()等,同时它存储了需要解释的指令并记录了每一次解释的当前标记(Token),而具体的解释过程交给表达式解释器类来处理。我们还可以将各种解释器类包含的公共方法移至环境类中,更好地实现这些方法的重用和扩展。

  1. 抽象表达式
/**
 * 抽象节点表达式
 */
abstract class Node {

    //声明一个方法用于解释语句,解析的过程就是转换为对应的对象
    public abstract void interpret(Context context);

    //声明一个方法用于执行标记对应的命令
    public abstract void execute();
}
  1. 非终结符表达式
/**
 * 表达式节点类:非终结符表达式
 * 对应的文法规则:
 *  expression ::= command* //表达式,一个表达式包含多条命令
 */
class ExpressionNode extends Node {
    private List<Node> list = new ArrayList<>();

    @Override
    public void interpret(Context context) {
        // 循环处理Context中的标记
        while (true) {
            // 如果已经没有任何标记,则退出解释
            if (context.currentToken() == null) {
                break;
            } else if (context.currentToken().equals("END")) {
                // 如果标记为END,则不解释END并结束本次解释过程。可以继续之后的解释
                context.skipToken("END");
                break;
            } else {
                //如果为其他标记,则解释标记并将其加入命令集合
                Node commandNode = new CommandNode();
                commandNode.interpret(context);
                list.add(commandNode);
            }
        }
    }

    //循环执行命令集合中的每一条命令
    @Override
    public void execute() {
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            ((Node)iterator.next()).execute();
        }
    }
}

/**
 * 语句命令节点类:非终结符表达式
 * 对应的文法规则:
 *  command ::= loop | primitive //语句命令
 */
class CommandNode extends Node {
    private Node node;

    @Override
    public void interpret(Context context) {
        // 处理LOOP循环命令
        if (context.currentToken().equals("LOOP")) {
            node = new LoopCommandNode();
            node.interpret(context);
        } else {
            // 处理其他基本命令
            node = new PrimitiveCommandNode();
            node.interpret(context);
        }
    }

    @Override
    public void execute() {
        node.execute();
    }
}

/**
 * 循环命令节点类:非终结符表达式
 * 对应的文法规则:
 *  loop ::= 'loopnumber' expression 'end' //循环命令,其中number为自然数
 */
class LoopCommandNode extends Node {

    //循环次数
    private int number;

    //循环语句中的表达式
    private Node commandNode;

    //解释循环命令
    @Override
    public void interpret(Context context) {
        context.skipToken("LOOP");
        number = context.currentNumber();
        context.nextToken();
        //循环语句中的表达式
        commandNode = new ExpressionNode();
        commandNode.interpret(context);
    }

    @Override
    public void execute() {
        for (int i = 0; i < number; i++) {
            commandNode.execute();
        }
    }
}
  1. 终结符表达式
/**
 * 基本命令节点类:终结符表达式
 * 对应的文法规则:
 *  primitive ::= 'printstring' | 'space' | 'break' //基本命令,其中string为字符串
 */
class PrimitiveCommandNode extends Node {

    private String name;
    private String text;

    //解释基本命令
    @Override
    public void interpret(Context context) {
        name = context.currentToken();
        context.skipToken(name);

        if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals ("SPACE")) {
            System.err.println("非法命令!");
        }

        if (name.equals("PRINT")) {
            text = context.currentToken();
            context.nextToken();
        }

    }

    @Override
    public void execute() {
        switch (this.name) {
            case "PRINT" : System.out.print(text); break;
            case "SPACE" : System.out.print(" "); break;
            case "BREAK" : System.out.println(); break;
        }
    }
}
  1. 客户端测试类
/**
 * @author liucheng
 * @since 2019/9/4
 */
public class Client {
    public static void main(String[] args) {
        String text = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉";
        Context context = new Context(text);

        Node node = new ExpressionNode();
        node.interpret(context);
        node.execute();
    }
}

五、总结

1). 优点

  1. 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  2. 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
  3. 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。
  4. 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。

2). 缺点

  1. 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
  2. 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

3). 适用场景

  1. 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
  2. 一些重复出现的问题可以用一种简单的语言来进行表达。
  3. 一个语言的文法较为简单。
  4. 执行效率不是关键问题。【注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。】
上一篇下一篇

猜你喜欢

热点阅读