解释器模式

总结自:解释器模式是什么?如何解析特定语言

解释器模式是一个专门用来处理语言或者解析表达式的设计模式。平时我们经常接触到的 SQL 语句和正则表达式的解析其实都用到了解释器模式。

入门案例

在讲解解释器模式之前,我们先来看一个简单的案例。

比如我们设计自己的一套语言:

REPEAT 3 TIMES: PRINT Hello

这个语句表示我们要打印 Hello 三次。

我们先来看一下简单版本的解析这段语句的代码:

public class SimpleInterpreter1 {
    public static void main(String[] args) {
        String command = "REPEAT 3 TIMES: PRINT Hello";

        // Split the command into words based on whitespace
        String[] words = command.split(" ");

        // Handle the command
        if (words[0].equalsIgnoreCase("REPEAT")) {
            int repeatCount = Integer.parseInt(words[1]);
            for (int i = 0; i < repeatCount; i++) {
                if (words[3].equalsIgnoreCase("PRINT")) {
                    System.out.println(words[4]);
                }
            }
        }
    }
}

它主要的逻辑是先创建 command,然后根据空格把命令分割成单词,然后我们先检查我们的第一个单词是不是 REPEAT,如果是的话我们就读取它重复的次数(这个例子里面重复的次数是 3),读取到重复次数之后,使用 for 循环循环指定次数,在 for 循环里面,检查我们的命令中有没有包含 PRINT,如果是 PRINT 的话我们就按照指定的次数打印需要的文本。

这个设计的主要缺点就是不容易拓展,因为我们如果要添加新的命令或者功能的话,就需要在这段代码里面修改主要的逻辑。

我们接下来看一下使用解释器模式改进之后的代码:

// AbstractExpression
interface Expression {
    void interpret();
}

// TerminalExpression for PRINT
class PrintExpression implements Expression {
    private String message;

    public PrintExpression(String message) {
        this.message = message;
    }

    @Override
    public void interpret() {
        System.out.println(message);
    }
}

// NonTerminalExpression for REPEAT
class RepeatExpression implements Expression {
    private int repeatCount;
    private Expression expression;

    public RepeatExpression(int repeatCount, Expression expression) {
        this.repeatCount = repeatCount;
        this.expression = expression;
    }

    @Override
    public void interpret() {
        for (int i = 0; i < repeatCount; i++) {
            expression.interpret();
        }
    }
}

// Client class
public class SimpleInterpreter2 {
    public static void main(String[] args) {
        String command = "REPEAT 3 TIMES: PRINT Hello";

        // Split the command into words based on whitespace
        String[] words = command.split(" ");

        //Handle the command
        if (words[0].equalsIgnoreCase("REPEAT")) {
            int repeatCount = Integer.parseInt(words[1]);

            // Create the TerminalExpression for PRINT
            Expression printExpression = new PrintExpression(words[4]);

            // Create the NonTerminalExpression for REPEAT
            Expression repeatExpression = new RepeatExpression(repeatCount, printExpression);

            // Interpret the command
            repeatExpression.interpret();
        }
    }
}

我们首先创建了一个抽象的 Expression 接口,所有的表达式它都会实现这个接口,它定义的 interpret 方法用于解释命令。

我们接下来看真正的表达式,PrintExpression 就是一个真正的表达式,它实现了 Expression 接口,用来处理 PRINT 命令。它包含一个 message 成员变量,用于存储要打印的信息,在 interpret 这个方法中,它就把这个 message 打印出来。

然后是 RepeatExpression,它也实现了 Expression 接口,这个表达式主要是用来处理 REPEAT 命令,它包含了一个 repeatCount 成员变量,用于存储重复的次数,还包含了一个 expression 类型的成员变量用于存储需要重复的表达式。在解释方法中,它就将 expression 解释 repeatCount 次。

客户端代码中,我们先创建命令并根据空格分割成字符串数组。然后处理命令的主要逻辑:如果命令是 REPEAT 开头,我们就读取 REPEAT 的次数,创建一个 PrintExpression 实例用于处理 print 命令,再创建一个 RepeatExpression 用于处理 REPEAT 命令,把 repeatCount 和 PrintExpression 放进去。执行时,调用 RepeatExpression 的 interpret 方法。

这种设计允许我们把解析和解释的关注点分开,使代码更加模块化、可维护、且易于拓展。对于上面的例子,客户端负责解析,各 Expression 实现负责解释。如果哪天需要在语句里添加新的表达式,或者修改现有的 PrintExpression 或 RepeatExpression 的逻辑,只需创建一个新的 Expression 或者修改当前的 Expression,而不会影响 Client 这边的整体架构。如果使用简单的设计,每次做改动都需要修改 main 里面的内容,表达式越多,代码就越复杂且难以维护。总之,对更复杂的语言和表达式,解释器模式非常有用,因为它允许我们更好地分离关注点,并创建更易于维护的代码。

特点和模式架构

如果你还没有完全理解,不用担心,毕竟解释器模式算是比较难懂的一个模式。我们等会儿还会有一个案例帮助大家更好地理解。接下来我们继续讨论解释器模式的特征和模式架构。

解释器模式的核心作用是用于处理语言语法或表达式,特别适合处理特定领域的语言。在解释器模式的代码架构中,主要有以下几个组件:

  1. AbstractExpression:定义了 interpret 方法,用于解释特定的表达式。
  2. TerminalExpression:实现了这个接口,表示语法中的终端符号,不再继续分解成更小的表达式。比如 PrintExpression 就是 TerminalExpression
  3. NonTerminalExpression:也实现了这个接口,表示语法中的非终端符号,它们表示对其他表达式的组合或操作,可以被分解成更小的表达式,直到被分解成终端表达式。比如 RepeatExpression 就是 NonTerminalExpression
  4. Context:包含解释过程中可能需要的信息或变量。
  5. Client:用来构建语法树,组装表达式,并按照特定的顺序调用它们的 interpret 方法来解析和执行语言。刚才的 main function 里面就是 client

案例扩展

接下来我们来看一个算术表达式的例子,它实现了一个简单的算术表达式解释器,可以解释和计算包含变量、数字和加法操作的表达式。

我们先看代码:

import java.util.HashMap;
import java.util.Map;

// AbstractExpression
interface Expression {
    int interpret(Context context);
}

// TerminalExpression for Numbers
class NumberExpression implements Expression {
    private int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret(Context context) {
        return number;
    }
}

// TerminalExpression for Variables
class VariableExpression implements Expression {
    private String variableName;

    public VariableExpression(String variableName) {
        this.variableName = variableName;
    }

    @Override
    public int interpret(Context context) {
        return context.get(variableName);
    }
}

// NonTerminalExpression for Addition
class AddExpression implements Expression {
    private Expression left;
    private Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Context context) {
        return left.interpret(context) + right.interpret(context);
    }
}

// Context class
class Context {
    private Map<String, Integer> variables = new HashMap<>();

    public void set(String name, int value) {
        variables.put(name, value);
    }

    public int get(String name) {
        return variables.get(name);
    }
}

// Client class
public class ArithmeticInterpreter {
    public static void main(String[] args) {
        // Set up the context
        Context context = new Context();
        context.set("x", 5);
        context.set("y", 7);

        // Build the expression tree
        Expression x = new VariableExpression("x");
        Expression y = new VariableExpression("y");
        Expression sum = new AddExpression(x, y);

        // Interpret the expression
        int result = sum.interpret(context);

        System.out.println("x + y = " + result); // Output: x + y = 12
    }
}

在代码中,我们先定义了一个 Expression 接口,这是我们的 AbstractExpression 组件。这个接口包含了 interpret 方法,接受一个 Context 类型的参数并返回一个整数。基于这个接口,我们实现了三个具体的表达式:

  1. TerminalExpressionNumberExpression,包含一个私有整数变量 number,构造函数接受一个整数参数用于设置 number 的值,interpret 方法返回 number 的值。
  2. ValuableExpression:是另一个终端表达式,实现 Expression 接口,包含一个私有字符串变量 valuableName,构造函数接受一个字符串参数用于设置 valuableName,在 interpret 方法中从 context 获取变量的值得到对应的整数值。
  3. NonTerminalExpressionAddExpression,包含两个私有变量 leftright,都是 Expression 类型,构造函数接受两个 Expression 类型的参数,interpret 方法对 leftright 的结果进行相加操作并返回。

context 用来存储变量的名字和对应的整数值,包含一个 HashMap 类型的 valuables,设置了 set 和 get 方法用于设置和获取变量的值。在客户端代码中,我们先创建一个 Context,设置 x 和 y 的值,然后创建 ValuableExpression 对象 x 和 y,再创建一个 AddExpression,把 x 和 y 放进去,最后调用 sum 的解释方法,把 context 放进去,就能获得计算表达式的结果,运行结果是 5 加 7 得 12。

接下来我们结合架构图分析各组件:

  1. AbstractExpression:即例子中的 Expression 接口,定义了 interpret 方法。
  2. TerminalExpression:包括 NumberExpressionValuableExpression,表示语法中的终端符号。
  3. NonTerminalExpression:用来组合其他表达式,例子中是 AddExpression,表示加法操作。
  4. Context:存取变量名和对应的整数值。
  5. Client:构建抽象语法树并调用表达式的 interpret 方法。

解释器模式允许我们组合这些元素来创建和计算更复杂的表达式,易于拓展语法系统,并支持其他运算符如减法、乘法或除法。

总结

解释器模式主要用于处理具有特定语法的文本输入,它的优点包括:

  • 能在现有语法基础上引入新语言模式,易于拓展和修改。
  • 允许开发人员定义特定问题的语言,更高效地解决复杂问题。
  • 将语言的语法结构和执行逻辑分离,便于代码管理和维护,促进代码复用性。

但它也有缺点:

  • 处理复杂语法时,解释过程可能比直接执行编译后代码慢。
  • 解释过程抽象不透明,调试难度大,可能增加代码复杂度和开发人员学习成本。
  • 不适用于所有问题,简单任务中使用可能效率低。

适合使用解释器模式的场景包括设计领域特定语言、解析复杂文本输入、实现灵活可拓展的语言结构等。常见的应用场景有领域特定语言的实现、编译语言和脚本语言的编译器或解释器、解析应用程序的外部配置文件、数据库系统和搜索引擎中的查询语言解析等。

posted @ 2024-06-26 15:36  Higurashi-kagome  阅读(2)  评论(0编辑  收藏  举报