1. 概要

このチュートリアルでは、動作GoFデザインパターンの1つであるインタープリターを紹介します。

最初に、その目的の概要を説明し、解決しようとしている問題について説明します。

次に、インタプリタのUML図と実際の例の実装を見ていきます。

2. インタプリタのデザインパターン

つまり、パターンは、特定の言語の文法を、インタプリタ自体が評価できるオブジェクト指向の方法で定義します。

そのことを念頭に置いて、技術的には、カスタム正規表現、カスタムDSLインタープリターを構築するか、任意の人間の言語を解析し、抽象構文ツリーを構築してから解釈を実行することができます。

これらは潜在的なユースケースのほんの一部ですが、しばらく考えてみると、たとえばIDEで、作成中のコードを継続的に解釈して提供しているため、さらに多くの使用法を見つけることができます。貴重なヒント。

文法が比較的単純な場合は、通常、インタープリターパターンを使用する必要があります。

そうしないと、保守が困難になる可能性があります。

3. UML図

上の図は、ContextExpressionの2つの主要なエンティティを示しています。

さて、どんな言語も何らかの形で表現する必要があり、単語(表現)は与えられた文脈に基づいて何らかの意味を持つことになります。

AbstractExpression は、コンテキストをパラメーターとして受け取る1つの抽象メソッドを定義します。 そのおかげで、各式はコンテキストに影響を与え、その状態を変更して、解釈を続行するか、結果自体を返します。

したがって、コンテキストは処理のグローバル状態のホルダーになり、解釈プロセス全体で再利用されます。

では、TerminalExpressionNonTerminalExpressionの違いは何ですか?

NonTerminalExpression には、1つ以上の他の AbstractExpressions が関連付けられている可能性があるため、再帰的に解釈できます。 最終的に、解釈のプロセスは、結果を返すTerminalExpressionで終了する必要があります。

NonTerminalExpressionコンポジットであることに注意してください。

最後に、クライアントの役割は、作成済みの抽象構文木を作成または使用することです。これは、作成された言語で定義された文にすぎません。

4. 実装

パターンの動作を示すために、オブジェクト指向の方法で単純なSQLのような構文を作成し、それを解釈して結果を返します。

まず、 Select、From、 Where 式を定義し、クライアントのクラスに構文ツリーを構築して、解釈を実行します。

Expression インターフェースには、次の解釈メソッドがあります。

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の別の具象Expressionを取得します。

オーバーライドされた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句はオプションであるため、このクラスは終端記号または非終端記号のいずれかです。

ユーザーがwhere句を使用しないことを決定した場合、 From 式は、 ctx.search()呼び出しで終了し、結果を返します。 そうでなければ、それはさらに解釈されるでしょう。

Where 式は、必要なフィルターを設定してコンテキストを再度変更し、検索呼び出しで解釈を終了します。

class Where implements Expression {

    private Predicate<String> filter;

    // constructor

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

たとえば、 Context クラスは、データベーステーブルを模倣しているデータを保持します。

Expressionの各サブクラスと検索メソッドによって変更される3つのキーフィールドがあることに注意してください。

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;
    }
}

検索が完了すると、コンテキストが自動的にクリアされるため、列、テーブル、およびフィルターがデフォルトに設定されます。

そうすれば、それぞれの解釈が他の解釈に影響を与えることはありません。

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プロジェクトで入手できます。