《一天一模式》— 解释器模式

原创
2020/06/12 00:33
阅读数 203

一、解释器模式的概念

给定一个语言,定义一个文法的一种表示, 并定义一个解释器, 这个解释器使用该表示来解释语言中的句子。

二、什么时候使用解释器模式

解释器模式是23种设计模式中唯一一种处理语法解析的设计模式。

当需要求是,需要解释某种语法来执行业务,并且你可将该语言中的句子表示为一个抽象语法树,可以使用解释器模式。

向《图解设计模式》里在解释器模式这章节提到的例子,使用BNF式描述语法,例子是通过一种语法来移动小车:

<program> ::= program <command list>
<command list> ::= <command>* end
<command> ::= <repeat command> | <primitive command>
<repeat command> ::= repeat <number> <command list>
<primitive command> ::= go | right | left

这种语法就可以做成抽象语法树,比如要控制小车走路,可以输入语法:program repeat 3 go right end end。

意思为重复执行3次go和right。

下面看看如何使用Java程序实现解释器模式的。

三、怎么使用解释器模式

3.1 实现方式

// 语法树节点的抽象类
public abstract class Node {

    abstract void parse(Context context);

}

// <program> ::= program <command list>
public class ProgramNode extends Node {

    private Node commandListNode;

    void parse(Context context) {
        // 解析程序命令,必须是program才可以向下执行
        // 等于从命令中跳过program
        context.skipToken("program");
        // 将后面的命令交给<command list>去解析
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

    public String toString() {
        return "[program " + commandListNode + "]";
    }

}

// <command list> ::= <command>* end
public class CommandListNode extends Node {

    ArrayList list = new ArrayList();

    void parse(Context context) {
        // 命令是由<command>* end组成的,所以要循环把*都解析,直到end
        while (true) {
            if (context.currentToken() == null) { // 缺少end报错
                throw new IllegalArgumentException("missing end");
            } else if (context.currentToken().equals("end")) { // 解析end,跳出循环
                context.skipToken("end");
                break;
            } else { // 将命令交给<command>解析
                Node commandNode = new CommandNode();
                commandNode.parse(context);
                // 便于打印结果定义了一个集合存放命令
                list.add(commandNode);
            }
        }
    }

    public String toString() {
        return list.toString();
    }

}

// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {

    private Node node;

    void parse(Context context) {
        // 判断是<repeat command> 还是 <primitive command>,然后找到想对应的解析对象
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }

    public String toString() {
        return node.toString();
    }

}

// <repeat command> ::= repeat <number> <command list>
public class RepeatCommandNode extends Node {

    int number;
    private Node commandListNode;

    void parse(Context context) {
        // 解析repeat,在获得number用于打印,然后把number也解析(用的nextToken)
        context.skipToken("repeat");
        number = context.currentNumber();
        context.nextToken();
        // 接着将命令交给<command list>
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

    public String toString() {
        return "[repeat +" + number + " " + commandListNode + "]";
    }

}

// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {

    String name;

    void parse(Context context) {
        // 解析这个命令
        name = context.currentToken();
        context.skipToken(name);
        // 如果不识别就报错
        if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
            throw new IllegalArgumentException("token is undefined.");
        }
    }

    public String toString() {
        return name;
    }

}

public class Context {

    StringTokenizer tokenizer;
    String currentToken;

    public Context(String text) {
        tokenizer = new StringTokenizer(text);
        nextToken();
    }

    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken = null;
        }
        return currentToken;
    }

    public String currentToken() {
        return currentToken;
    }

    public void skipToken(String token) {
        if (!token.equals(currentToken)) {
            throw new IllegalArgumentException("token is expected, but currentToken is found.");
        }
        nextToken();
    }

    public int currentNumber() {
        int number = 0;
        try {
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("number is bad param.");
        }
        return number;
    }

}

使用方法如下:

public class Client {

    public static void main(String[] args) {
        String command5 = "program repeat 4 repeat 3 go right go right end right end end";
        Node node = new ProgramNode();
        node.parse(new Context(command5));
        System.out.println(node);
    }

}

// 输出
// [program [[repeat +4 [[repeat +3 [go, right, go, right]], right]]]]

3.2 解释器模式的好处

  • 可以很容易地改变和扩展方法, 因为该模式使用类来表示方法规则, 你可以使用继承来改变或扩展该方法;
  • 也比较容易实现方法, 因为定义抽象语法树总各个节点的类的实现大体类似, 这些类都易于直接编写;
  • 解释器模式就是将一句话,转变为实际的命令程序执行而已。 而不用解释器模式本身也可以分析, 但通过继承抽象表达式的方式, 由于依赖转置原则, 使得文法的扩展和维护都带来的方便;

3.3 解释器模式需要注意的地方

解释器模式为方法中的每一条规则至少定义了一个类, 因此包含许多规则的方法可能难以管理和维护。 因此当方法非常复杂时, 使用其他的技术如 语法分析程序 或 编译器生成器来处理。

四、总结

解释器模式的关键在于如何将需求抽象成语法树。

其业务实现很简单,用类来解析每个命令。

解释器模式在实际的系统开发中使用的非常少,因为它会引起效率、性能以及维护等问题,一般在大中型的框架型项目能够找到它的身影,比如一些数据分析工具、报表设计工具、科学计算工具等等

以上就是我对解释器模式的一些理解,有不足之处请大家指出,谢谢。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部