Java中的解释器设计模式

2023/05/26

1. 概述

在本教程中,我们介绍GoF行为型设计模式之一:解释器。

首先,我们概述其目的并解释它试图解决的问题。然后,我们看看解释器模式的UML图和实际示例的实现。

2. 解释器设计模式

简而言之,该模式以面向对象的方式定义了特定语言的语法,可以由解释器本身进行评估

考虑到这一点,从技术上讲,我们可以构建自定义正则表达式、自定义DSL解释器,或者我们可以解析任何人类语言,构建抽象语法树,然后运行解释

这些只是一些潜在的用例,但如果我们仔细想一想,我们会发现它的更多用途,例如在我们的IDE中,因为它们不断地解释我们正在编写的代码,从而为我们提供无价的提示。

解释器模式一般应该在语法比较简单的时候使用。否则,它可能会变得难以维护。

3. UML图

上图显示了两个主要实体:上下文和表达式。

现在,任何语言都需要以某种方式表达,并且单词(表达式)将根据给定的上下文具有某种含义。

AbstractExpression定义了一个将上下文作为参数的抽象方法。因此,每个表达式都会影响上下文,改变其状态并继续解释或返回结果本身。

因此,上下文将成为全局处理状态的持有者,并且将在整个解释过程中重复使用。

那么TerminalExpression和NonTerminalExpression有什么区别呢?

NonTerminalExpression可能关联一个或多个其他AbstractExpressions,因此它可以被递归解释。最后,解释过程必须以返回结果的TerminalExpression完成

值得注意的是NonTerminalExpression是一个复合的。

最后,客户端的作用是创建或使用一个已经创建的抽象语法树,它无非是在创建的语言中定义的一个句子

4. 实现

为了演示该模式的实际应用,我们将以面向对象的方式构建一个简单的类似SQL的语法,然后对其进行解释并返回结果。

首先,我们定义Select、From和Where表达式,在客户端类中构建语法树并运行解释。

Expression接口具有interpret方法:

List<String> interpret(Context ctx);

接下来,我们定义第一个表达式,即Select类:

class Select implements Expression {

    private String column;
    private From from;

    // constructor

    @Override
    public List<String> interpret(Context ctx) {
        ctx.setColumn(column);
        return from.interpret(ctx);
    }
}

它获取要选择的列名称和另一个具体的From类型的表达式作为构造函数中的参数。

请注意,在重写的interpret()方法中,它设置了上下文的状态,并将解释与上下文一起进一步传递给另一个表达式。

这样,我们看到它是一个NonTerminalExpression。

另一个表达式是From类:

class From implements Expression {

    private String table;
    private Where where;

    // constructors

    @Override
    public List<String> interpret(Context ctx) {
        ctx.setTable(table);
        if (where == null) {
            return ctx.search();
        }
        return where.interpret(ctx);
    }
}

现在,在SQL中,where子句是可选的,因此此类是TerminalExpression或NonTerminalExpression。

如果用户决定不使用where子句,则From表达式将以ctx.search()调用终止并返回结果;否则它将被进一步解释。

Where表达式再次通过设置必要的过滤器来修改上下文,并通过search调用终止解释:

class Where implements Expression {

    private Predicate<String> filter;

    // constructor

    @Override
    public List<String> interpret(Context ctx) {
        ctx.setFilter(filter);
        return ctx.search();
    }
}

例如,Context类保存模仿数据库表的数据。

请注意,它具有三个关键字段,它们由Expression的每个子类和search方法修改:

class Context {

    private static Map<String, List<Row>> tables = new HashMap<>();

    static {
        List<Row> list = new ArrayList<>();
        list.add(new Row("John", "Doe"));
        list.add(new Row("Jan", "Kowalski"));
        list.add(new Row("Dominic", "Doom"));

        tables.put("people", list);
    }

    private String table;
    private String column;
    private Predicate<String> whereFilter;

    // ... 

    List<String> search() {
        List<String> result = tables.entrySet()
                .stream()
                .filter(entry -> entry.getKey().equalsIgnoreCase(table))
                .flatMap(entry -> Stream.of(entry.getValue()))
                .flatMap(Collection::stream)
                .map(Row::toString)
                .flatMap(columnMapper)
                .filter(whereFilter)
                .collect(Collectors.toList());

        clear();

        return result;
    }
}

搜索完成后,上下文会自行清除,因此column、table和whereFilter都设置为默认值,这样每个解释之间不会相互影响。

5. 测试

出于测试目的,让我们看一下InterpreterDemo类:

public class InterpreterDemo {
    
    public static void main(String[] args) {
        Expression query = new Select("name", new From("people"));
        Context ctx = new Context();
        List<String> result = query.interpret(ctx);
        System.out.println(result);

        Expression query2 = new Select("", new From("people"));
        List<String> result2 = query2.interpret(ctx);
        System.out.println(result2);

        Expression query3 = new Select("name",
                new From("people", new Where(name -> name.toLowerCase().startsWith("d"))));
        List<String> result3 = query3.interpret(ctx);
        System.out.println(result3);
    }
}

首先,我们使用创建的表达式构建语法树,初始化上下文,然后运行解释。上下文是重用的,但正如我们上面所演示的,它会在每次搜索调用后自行清理。

通过运行该程序,输出应如下所示:

[John, Jan, Dominic]
[John Doe, Jan Kowalski, Dominic Doom]
[Dominic]

6. 缺点

当语法变得越来越复杂时,它变得更难维护。

从给出的例子中可以看出,添加另一个表达式(如Limit)会相当容易,但如果我们决定用所有其他表达式继续扩展它,则维护起来不会太容易。

7. 总结

解释器设计模式非常适合相对简单的语法解释,不需要太多的重构和扩展

在上面的例子中,我们演示了在解释器模式的帮助下以面向对象的方式构建类似SQL的查询是可能的。

最后,你可以在JDK中找到这种模式的用法,特别是在java.util.Pattern、java.text.Format或java.text.Normalizer类中。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章